diff --git a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py
index 9d1c254d1a7..55746e5e527 100644
--- a/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py
+++ b/litellm/proxy/guardrails/guardrail_hooks/litellm_content_filter/content_filter.py
@@ -329,10 +329,10 @@ class ContentFilterGuardrail(CustomGuardrail):
action if action else category_config_obj.default_action
)
- # Handle conditional categories (with identifier_words + inherit_from OR identifier_words + additional_block_words)
- if category_config_obj.identifier_words and (
- category_config_obj.inherit_from
- or category_config_obj.additional_block_words
+ # Handle conditional categories (with identifier_words + inherit_from)
+ if (
+ category_config_obj.identifier_words
+ and category_config_obj.inherit_from
):
self._load_conditional_category(
category_name,
@@ -387,81 +387,64 @@ class ContentFilterGuardrail(CustomGuardrail):
categories_dir: str,
) -> None:
"""
- Load a conditional category that uses identifier_words + block_words.
-
- Supports two patterns:
- 1. Inherit + additional: identifier_words + inherit_from + optional additional_block_words
- 2. Standalone: identifier_words + additional_block_words (no inheritance)
+ Load a conditional category that uses identifier_words + inherited block_words.
Args:
category_name: Name of the category
- category_config_obj: CategoryConfig object with identifier_words and either inherit_from or additional_block_words
+ category_config_obj: CategoryConfig object with identifier_words and inherit_from
category_action: Action to take when match is found
severity_threshold: Minimum severity threshold
categories_dir: Directory containing category files
"""
- block_words = []
+ # Load the inherited category to get block words
inherit_from = category_config_obj.inherit_from
+ if not inherit_from:
+ return
- # Pattern 1: Load inherited category to get base block words
- if inherit_from:
- # Remove .json or .yaml extension if included
- inherit_base = inherit_from.replace(".json", "").replace(".yaml", "")
+ # Remove .json or .yaml extension if included
+ inherit_base = inherit_from.replace(".json", "").replace(".yaml", "")
- # Find the inherited category file
- inherit_yaml_path = os.path.join(categories_dir, f"{inherit_base}.yaml")
- inherit_json_path = os.path.join(categories_dir, f"{inherit_base}.json")
+ # Find the inherited category file
+ inherit_yaml_path = os.path.join(categories_dir, f"{inherit_base}.yaml")
+ inherit_json_path = os.path.join(categories_dir, f"{inherit_base}.json")
- if os.path.exists(inherit_yaml_path):
- inherit_file_path = inherit_yaml_path
- elif os.path.exists(inherit_json_path):
- inherit_file_path = inherit_json_path
- else:
- verbose_proxy_logger.warning(
- f"Category {category_name}: inherit_from '{inherit_from}' file not found at {categories_dir}"
- )
- verbose_proxy_logger.debug(
- f"Tried paths: {inherit_yaml_path}, {inherit_json_path}"
- )
- return
-
- try:
- # Load the inherited category
- inherited_category = self._load_category_file(inherit_file_path)
-
- # Extract block words from inherited category that meet severity threshold
- for keyword_data in inherited_category.keywords:
- keyword = keyword_data["keyword"].lower()
- severity = keyword_data["severity"]
- if self._should_apply_severity(severity, severity_threshold):
- block_words.append(keyword)
- except Exception as e:
- verbose_proxy_logger.error(
- f"Error loading inherited category for {category_name}: {e}"
- )
- return
-
- # Pattern 2 or supplement to Pattern 1: Add additional block words
- if category_config_obj.additional_block_words:
- block_words.extend(category_config_obj.additional_block_words)
-
- # Ensure we have block words before storing
- if not block_words:
+ if os.path.exists(inherit_yaml_path):
+ inherit_file_path = inherit_yaml_path
+ elif os.path.exists(inherit_json_path):
+ inherit_file_path = inherit_json_path
+ else:
verbose_proxy_logger.warning(
- f"Category {category_name}: no block words found (check inherit_from or additional_block_words)"
+ f"Category {category_name}: inherit_from '{inherit_from}' file not found at {categories_dir}"
+ )
+ verbose_proxy_logger.debug(
+ f"Tried paths: {inherit_yaml_path}, {inherit_json_path}"
)
return
- # Store the conditional category configuration
- self.conditional_categories[category_name] = {
- "identifier_words": category_config_obj.identifier_words,
- "block_words": block_words,
- "action": category_action,
- "severity": "high", # Combinations are always high severity
- }
+ try:
+ # Load the inherited category
+ inherited_category = self._load_category_file(inherit_file_path)
+
+ # Extract block words from inherited category that meet severity threshold
+ block_words = []
+ for keyword_data in inherited_category.keywords:
+ keyword = keyword_data["keyword"].lower()
+ severity = keyword_data["severity"]
+ if self._should_apply_severity(severity, severity_threshold):
+ block_words.append(keyword)
+
+ # Add additional block words specific to this category
+ if category_config_obj.additional_block_words:
+ block_words.extend(category_config_obj.additional_block_words)
+
+ # Store the conditional category configuration
+ self.conditional_categories[category_name] = {
+ "identifier_words": category_config_obj.identifier_words,
+ "block_words": block_words,
+ "action": category_action,
+ "severity": "high", # Combinations are always high severity
+ }
- # Log different messages based on pattern
- if inherit_from and category_config_obj.additional_block_words:
verbose_proxy_logger.info(
f"Loaded conditional category {category_name}: "
f"{len(category_config_obj.identifier_words)} identifiers + "
@@ -469,17 +452,9 @@ class ContentFilterGuardrail(CustomGuardrail):
f"({len(category_config_obj.additional_block_words)} additional + "
f"{len(block_words) - len(category_config_obj.additional_block_words)} from {inherit_from})"
)
- elif inherit_from:
- verbose_proxy_logger.info(
- f"Loaded conditional category {category_name}: "
- f"{len(category_config_obj.identifier_words)} identifiers + "
- f"{len(block_words)} block words (from {inherit_from})"
- )
- else:
- verbose_proxy_logger.info(
- f"Loaded conditional category {category_name}: "
- f"{len(category_config_obj.identifier_words)} identifiers + "
- f"{len(block_words)} block words (standalone)"
+ except Exception as e:
+ verbose_proxy_logger.error(
+ f"Error loading inherited category for {category_name}: {e}"
)
def _load_category_file(self, file_path: str) -> CategoryConfig:
@@ -1398,6 +1373,41 @@ class ContentFilterGuardrail(CustomGuardrail):
names = [cat.description or cat.category_name for cat in self.loaded_categories.values()]
return ", ".join(names) if names else None
+ def _compute_risk_score(
+ self,
+ detections: List[ContentFilterDetection],
+ masked_entity_count: Dict[str, int],
+ status: "GuardrailStatus",
+ ) -> float:
+ """
+ Compute a risk score from 0-10 for this guardrail evaluation.
+
+ Factors:
+ - Match ratio: how many patterns matched vs total checked
+ - Number of entities masked
+ - Whether the guardrail blocked the request (max risk)
+ """
+ if status == "guardrail_intervened":
+ return 10.0
+
+ total_masked = sum(masked_entity_count.values()) if masked_entity_count else 0
+ patterns_checked = self._get_patterns_checked_count()
+
+ # Match ratio contribution (0-7 points)
+ match_ratio = total_masked / patterns_checked if patterns_checked > 0 else 0.0
+ ratio_score = match_ratio * 7.0
+
+ # Detection count contribution (0-3 points, capped)
+ detection_score = min(len(detections), 5) * 0.6
+
+ score = ratio_score + detection_score
+
+ # Floor: if anything matched, minimum risk is 2
+ if total_masked > 0 and score < 2.0:
+ score = 2.0
+
+ return round(min(10.0, score), 1)
+
def _log_guardrail_information(
self,
request_data: dict,
@@ -1444,6 +1454,7 @@ class ContentFilterGuardrail(CustomGuardrail):
detection_method=self._get_detection_methods(detections) if detections else None,
match_details=self._build_match_details(detections) if detections else None,
patterns_checked=self._get_patterns_checked_count(),
+ risk_score=self._compute_risk_score(detections, masked_entity_count, status),
),
)
diff --git a/litellm/types/utils.py b/litellm/types/utils.py
index 5fbfd23b2db..5f8798c7712 100644
--- a/litellm/types/utils.py
+++ b/litellm/types/utils.py
@@ -2644,6 +2644,9 @@ class StandardLoggingGuardrailInformation(TypedDict, total=False):
alert_recipients: Optional[List[str]]
"""Email addresses that were notified"""
+ risk_score: Optional[float]
+ """Risk score 0-10 indicating how risky the request was (higher = riskier). Computed by the guardrail provider."""
+
class GuardrailTracingDetail(TypedDict, total=False):
"""
@@ -2661,6 +2664,7 @@ class GuardrailTracingDetail(TypedDict, total=False):
match_details: Optional[List[dict]]
patterns_checked: Optional[int]
alert_recipients: Optional[List[str]]
+ risk_score: Optional[float]
StandardLoggingPayloadStatus = Literal["success", "failure"]
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 8f9869e4dd5..35828922012 100644
--- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
@@ -1,11 +1,13 @@
-import React, { useState } from "react";
-import { Tooltip, Collapse } from "antd";
+import React, { useState, useMemo } from "react";
+import { Tooltip } from "antd";
import PresidioDetectedEntities from "./PresidioDetectedEntities";
import BedrockGuardrailDetails, {
BedrockGuardrailResponse,
} from "@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails";
import ContentFilterDetails from "./ContentFilterDetails";
+// ── Interfaces ──────────────────────────────────────────────────────────────
+
interface RecognitionMetadata {
recognizer_name: string;
recognizer_identifier: string;
@@ -51,45 +53,204 @@ interface GuardrailInformation {
match_details?: MatchDetail[];
patterns_checked?: number;
alert_recipients?: string[];
+ risk_score?: number;
}
interface GuardrailViewerProps {
data: GuardrailInformation | GuardrailInformation[];
}
-interface GuardrailDetailsProps {
- entry: GuardrailInformation;
- index: number;
- total: number;
-}
+// ── Helpers ─────────────────────────────────────────────────────────────────
-const formatTime = (timestamp: number) => {
- const date = new Date(timestamp * 1000);
- return date.toLocaleString();
+const PROVIDERS_WITH_CUSTOM_RENDERERS = new Set([
+ "presidio",
+ "bedrock",
+ "litellm_content_filter",
+]);
+
+const formatMode = (mode: string): string => {
+ return mode.replace(/_/g, "-").toUpperCase();
};
-// Providers with custom renderers
-const PROVIDERS_WITH_CUSTOM_RENDERERS = new Set(["presidio", "bedrock", "litellm_content_filter"]);
+const formatDurationMs = (seconds: number): string => {
+ const ms = Math.round(seconds * 1000);
+ return `${ms}ms`;
+};
+
+const getTotalMasked = (entry: GuardrailInformation): number => {
+ return Object.values(entry.masked_entity_count || {}).reduce(
+ (sum, count) => sum + (typeof count === "number" ? count : 0),
+ 0,
+ );
+};
+
+const isEntrySuccess = (entry: GuardrailInformation): boolean => {
+ return (entry.guardrail_status ?? "").toLowerCase() === "success";
+};
+
+const getRiskColor = (score: number): string => {
+ if (score <= 3) return "text-green-600 bg-green-50 border-green-200";
+ if (score <= 6) return "text-amber-600 bg-amber-50 border-amber-200";
+ return "text-red-600 bg-red-50 border-red-200";
+};
+
+const getRiskScore = (entry: GuardrailInformation): number | null => {
+ if (!isEntrySuccess(entry)) return null;
+
+ // Prefer backend-computed score
+ if (entry.risk_score != null) return entry.risk_score;
+
+ // Fallback: compute from available data
+ const totalMasked = getTotalMasked(entry);
+ const patternsChecked = entry.patterns_checked ?? 0;
+ const confidence = entry.confidence_score ?? 0;
+
+ if (patternsChecked === 0 && confidence === 0) return 0;
+
+ const matchRatio = patternsChecked > 0 ? totalMasked / patternsChecked : 0;
+ let score = matchRatio * 7 + confidence * 3;
+ if (totalMasked > 0 && score < 2) score = 2;
+ return Math.min(10, Math.round(score * 10) / 10);
+};
+
+const getDisplayName = (entry: GuardrailInformation): string => {
+ return entry.policy_template || entry.guardrail_name;
+};
+
+// ── Icons (inline SVGs) ─────────────────────────────────────────────────────
+
+const ShieldIcon = () => (
+
+);
+
+const CheckCircleIcon = ({ className }: { className?: string }) => (
+
+);
+
+const FailCircleIcon = ({ className }: { className?: string }) => (
+
+);
+
+const PlayCircleIcon = () => (
+
+);
+
+const GrayDotIcon = () => (
+
+);
+
+const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
+
+);
+
+const DownloadIcon = () => (
+
+);
+
+const ExternalLinkIcon = () => (
+
+);
+
+// ── Sub-components ──────────────────────────────────────────────────────────
+
+const MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => {
+ if (!matchDetails || matchDetails.length === 0) return null;
+
+ return (
+
+
Match Details ({matchDetails.length})
+
+
+
+
+ | Type |
+ Method |
+ Action |
+ Detail |
+
+
+
+ {matchDetails.map((match, idx) => (
+
+ | {match.type} |
+
+
+ {match.detection_method ?? "-"}
+
+ |
+
+
+ {match.action_taken ?? "-"}
+
+ |
+
+ {match.category ? `[${match.category}] ` : ""}
+ {match.snippet ?? "-"}
+ |
+
+ ))}
+
+
+
+
+ );
+};
const GenericGuardrailResponse = ({ response }: { response: any }) => {
const [showRaw, setShowRaw] = useState(false);
return (
-
+
setShowRaw(!showRaw)}
>
-
-
Raw Guardrail Response
+
+
Raw Guardrail Response
{showRaw && (
@@ -104,189 +265,162 @@ const GenericGuardrailResponse = ({ response }: { response: any }) => {
);
};
-const PolicyDetectionRow = ({ entry }: { entry: GuardrailInformation }) => {
- const hasData = entry.policy_template || entry.detection_method || entry.confidence_score != null || entry.patterns_checked != null;
- if (!hasData) return null;
+// ── Timeline entry types ────────────────────────────────────────────────────
- return (
-
-
- {entry.policy_template && (
-
- Policy:
-
- {entry.policy_template}
-
-
- )}
- {entry.detection_method && (
-
- Detection:
- {entry.detection_method.split(",").map((method) => (
-
- {method.trim()}
-
- ))}
-
- )}
- {entry.confidence_score != null && (
-
- Confidence:
- = 0.8 ? "bg-red-100 text-red-800" :
- entry.confidence_score >= 0.5 ? "bg-amber-100 text-amber-800" :
- "bg-green-100 text-green-800"
- }`}>
- {(entry.confidence_score * 100).toFixed(0)}%
-
-
- )}
- {entry.patterns_checked != null && (
-
- Patterns checked:
- {entry.patterns_checked}
-
- )}
-
-
+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 MatchDetailsTable = ({ matchDetails }: { matchDetails: MatchDetail[] }) => {
- if (!matchDetails || matchDetails.length === 0) return null;
+ 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 (
-
-
Match Details ({matchDetails.length})
-
-
-
-
- | Type |
- Method |
- Action |
- Detail |
-
-
-
- {matchDetails.map((match, idx) => (
-
- | {match.type} |
-
-
- {match.detection_method ?? "-"}
-
- |
-
-
- {match.action_taken ?? "-"}
-
- |
-
- {match.category ? `[${match.category}] ` : ""}{match.snippet ?? "-"}
- |
-
- ))}
-
-
-
-
- );
-};
-
-const ClassificationDetails = ({ classification }: { classification: Record
}) => {
- if (!classification) return null;
-
- return (
-
-
Classification
-
- {classification.category && (
-
- Category:
- {classification.category}
-
- )}
- {classification.article_reference && (
-
- Reference:
- {classification.article_reference}
-
- )}
- {classification.confidence != null && (
-
- Confidence:
- {(classification.confidence * 100).toFixed(0)}%
-
- )}
- {classification.reason && (
-
- Reason:
- {classification.reason}
-
- )}
-
-
- );
-};
-
-const ExecutionTimeline = ({ entries }: { entries: GuardrailInformation[] }) => {
- if (entries.length <= 1) return null;
-
- const sorted = [...entries].sort((a, b) => (a.start_time ?? 0) - (b.start_time ?? 0));
-
- return (
-
-
Execution Timeline
-
- {sorted.map((e, idx) => {
- const isSuccess = (e.guardrail_status ?? "").toLowerCase() === "success";
- return (
-
-
-
-
- {e.duration?.toFixed(3)}s
-
-
{e.guardrail_name}
-
- {e.guardrail_mode}
-
-
- {e.guardrail_status}
-
- {e.policy_template && (
-
- {e.policy_template}
-
+
+
+ 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
+
+
+
+
+ ))}
);
};
-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,
- );
+// ── 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 =
@@ -297,201 +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}
-
- {entry.guardrail_id && entry.guardrail_id !== entry.guardrail_name && (
-
- Guardrail ID:
- {entry.guardrail_id}
-
+ return (
+
+ {/* Collapsed header row */}
+
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 9081219d5be..f0f4041531c 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}
+ ))}
);
}