diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx index cd36030e512..bbd0dda43f3 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx @@ -1,10 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useMemo } from "react"; import { Tooltip } from "antd"; import { - checkEuAiActCompliance, - checkGdprCompliance, ComplianceResponse, - ComplianceCheckRequest, } from "@/components/networking"; interface CompliancePanelProps { @@ -18,6 +15,102 @@ interface CompliancePanelProps { }; } +function runComplianceChecksLocally(logEntry: CompliancePanelProps["logEntry"]): { + euAiAct: ComplianceResponse; + gdpr: ComplianceResponse; +} { + const guardrails: Record[] = logEntry.metadata?.guardrail_information ?? []; + const hasGuardrails = guardrails.length > 0; + const preCallGuardrails = guardrails.filter( + (g) => g.guardrail_mode === "pre_call" || !g.guardrail_mode + ); + const hasPreCall = preCallGuardrails.length > 0; + const hasUser = Boolean(logEntry.user); + const hasModel = Boolean(logEntry.model); + const hasTimestamp = Boolean(logEntry.startTime); + const auditComplete = hasUser && hasModel && hasTimestamp && hasGuardrails; + + const missingFields: string[] = []; + if (!hasUser) missingFields.push("user_id"); + if (!hasModel) missingFields.push("model"); + if (!hasTimestamp) missingFields.push("timestamp"); + if (!hasGuardrails) missingFields.push("guardrail_results"); + + const hasIntervention = preCallGuardrails.some((g) => + ["guardrail_intervened", "failed", "blocked"].includes(g.guardrail_status ?? "") + ); + const allPassed = + preCallGuardrails.length > 0 && + preCallGuardrails.every((g) => g.guardrail_status === "success"); + const dataProtected = hasIntervention || allPassed; + + const euAiAct: ComplianceResponse = { + regulation: "EU AI Act", + compliant: hasGuardrails && hasPreCall && auditComplete, + checks: [ + { + check_name: "Guardrails applied", + article: "Art. 9", + passed: hasGuardrails, + detail: hasGuardrails + ? `${guardrails.length} guardrail(s) applied` + : "No guardrails applied", + }, + { + check_name: "Content screened before LLM", + article: "Art. 5", + passed: hasPreCall, + detail: hasPreCall + ? `${preCallGuardrails.length} pre-call guardrail(s) screened content` + : "No pre-call screening applied", + }, + { + check_name: "Audit record complete", + article: "Art. 12", + passed: auditComplete, + detail: auditComplete + ? "All required audit fields present" + : `Missing: ${missingFields.join(", ")}`, + }, + ], + }; + + const gdpr: ComplianceResponse = { + regulation: "GDPR", + compliant: hasPreCall && dataProtected && auditComplete, + checks: [ + { + check_name: "Data protection applied", + article: "Art. 32", + passed: hasPreCall, + detail: hasPreCall + ? `${preCallGuardrails.length} pre-call guardrail(s) protect data` + : "No pre-call data protection applied", + }, + { + check_name: "Sensitive data protected", + article: "Art. 5(1)(c)", + passed: dataProtected, + detail: hasIntervention + ? "Guardrail intervened to protect sensitive data" + : allPassed + ? "No sensitive data detected" + : "No pre-call guardrails to protect sensitive data", + }, + { + check_name: "Audit record complete", + article: "Art. 30", + passed: auditComplete, + detail: auditComplete + ? "All required audit fields present" + : `Missing: ${missingFields.join(", ")}`, + }, + ], + }; + + return { euAiAct, gdpr }; +} + // -- Icons -- const CheckIcon = () => ( @@ -136,38 +229,7 @@ const ComplianceCard = ({ // -- Main Component -- const CompliancePanel: React.FC = ({ accessToken, logEntry }) => { - const [euAiActData, setEuAiActData] = useState(null); - const [gdprData, setGdprData] = useState(null); - const [euAiActLoading, setEuAiActLoading] = useState(false); - const [gdprLoading, setGdprLoading] = useState(false); - const [euAiActError, setEuAiActError] = useState(null); - const [gdprError, setGdprError] = useState(null); - - useEffect(() => { - if (!accessToken || !logEntry.request_id) return; - - const payload: ComplianceCheckRequest = { - request_id: logEntry.request_id, - user_id: logEntry.user, - model: logEntry.model, - timestamp: logEntry.startTime, - guardrail_information: logEntry.metadata?.guardrail_information, - }; - - setEuAiActLoading(true); - setEuAiActError(null); - checkEuAiActCompliance(accessToken, payload) - .then(setEuAiActData) - .catch((err) => setEuAiActError(err.message || "Failed to check EU AI Act compliance")) - .finally(() => setEuAiActLoading(false)); - - setGdprLoading(true); - setGdprError(null); - checkGdprCompliance(accessToken, payload) - .then(setGdprData) - .catch((err) => setGdprError(err.message || "Failed to check GDPR compliance")) - .finally(() => setGdprLoading(false)); - }, [accessToken, logEntry]); + const { euAiAct, gdpr } = useMemo(() => runComplianceChecksLocally(logEntry), [logEntry]); return (
@@ -177,15 +239,15 @@ const CompliancePanel: React.FC = ({ accessToken, logEntry
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 913634d388f..1dfcf97134a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx @@ -165,6 +165,20 @@ export function LogDetailContent({ logEntry, onOpenSettings, isLoadingDetails = /> )} + {/* Compliance Panel – always visible */} +
+ +
+ {/* Guardrail Data */} {hasGuardrailData && (