From 5946a933a06a7fa5b955bbb4637cfd0e5b8d210f Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Tue, 17 Feb 2026 18:22:26 -0800 Subject: [PATCH] Add compliance checker endpoints + UI panel (#21432) * eu_ai_act_article5_prohibited_practices_fr * add backend for checkers * test checker * checkEuAiActCompliance * register compliance router in proxy_server.py * add compliance check functions to networking.tsx * fix useEffect dependency array in CompliancePanel * ui fixes --- litellm/proxy/compliance_checks.py | 221 ++++++++++ .../compliance_endpoints.py | 79 ++++ litellm/proxy/proxy_server.py | 4 + litellm/types/proxy/compliance_endpoints.py | 33 ++ .../test_compliance_endpoints.py | 387 ++++++++++++++++++ .../src/components/networking.tsx | 67 +++ .../GuardrailViewer/CompliancePanel.tsx | 195 +++++++++ .../GuardrailViewer/GuardrailViewer.tsx | 23 +- .../LogDetailsDrawer/LogDetailContent.tsx | 16 +- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 1 + 10 files changed, 1018 insertions(+), 8 deletions(-) create mode 100644 litellm/proxy/compliance_checks.py create mode 100644 litellm/proxy/management_endpoints/compliance_endpoints.py create mode 100644 litellm/types/proxy/compliance_endpoints.py create mode 100644 tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py create mode 100644 ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx diff --git a/litellm/proxy/compliance_checks.py b/litellm/proxy/compliance_checks.py new file mode 100644 index 0000000000..381b0f815d --- /dev/null +++ b/litellm/proxy/compliance_checks.py @@ -0,0 +1,221 @@ +""" +Compliance checker for EU AI Act and GDPR regulations. + +Provides guardrail-agnostic compliance validation based on guardrail modes +and execution results rather than specific guardrail names. +""" + +from typing import Dict, List + +from litellm.types.proxy.compliance_endpoints import ( + ComplianceCheckRequest, + ComplianceCheckResult, +) + + +class ComplianceChecker: + """ + Validates compliance with EU AI Act and GDPR regulations. + + Uses guardrail-agnostic checks based on: + - Whether any guardrails ran + - Guardrail execution mode (pre-call, post-call, etc.) + - Whether guardrails intervened/blocked content + - Completeness of audit records + """ + + def __init__(self, data: ComplianceCheckRequest): + self.data = data + self.guardrails = data.guardrail_information or [] + + def _get_guardrails_by_mode(self, mode: str) -> List[Dict]: + """ + Get all guardrails that ran in a specific mode. + + If a guardrail doesn't have a mode specified, it's treated as pre-call + (the most common case). + """ + result = [] + for g in self.guardrails: + g_mode = g.get("guardrail_mode") + # If no mode specified, default to pre_call + if g_mode is None and mode == "pre_call": + result.append(g) + elif g_mode == mode: + result.append(g) + return result + + def _has_guardrail_intervention(self, guardrails: List[Dict]) -> bool: + """Check if any guardrail intervened (blocked/masked content).""" + for g in guardrails: + status = g.get("guardrail_status", "") + if status in ["guardrail_intervened", "failed", "blocked"]: + return True + return False + + def _all_guardrails_passed(self, guardrails: List[Dict]) -> bool: + """Check if all guardrails passed (no issues detected).""" + if not guardrails: + return False + return all(g.get("guardrail_status") == "success" for g in guardrails) + + # ── EU AI Act Helper Methods ──────────────────────────────────────────── + + def _check_art_9_guardrails_applied(self) -> ComplianceCheckResult: + """Art. 9: Check if any guardrails were applied.""" + has_guardrails = len(self.guardrails) > 0 + return ComplianceCheckResult( + check_name="Guardrails applied", + article="Art. 9", + passed=has_guardrails, + detail=( + f"{len(self.guardrails)} guardrail(s) applied" + if has_guardrails + else "No guardrails applied" + ), + ) + + def _check_art_5_content_screened(self) -> ComplianceCheckResult: + """Art. 5: Check if content was screened before LLM (pre-call).""" + pre_call_guardrails = self._get_guardrails_by_mode("pre_call") + has_pre_call = len(pre_call_guardrails) > 0 + return ComplianceCheckResult( + check_name="Content screened before LLM", + article="Art. 5", + passed=has_pre_call, + detail=( + f"{len(pre_call_guardrails)} pre-call guardrail(s) screened content" + if has_pre_call + else "No pre-call screening applied" + ), + ) + + def _check_art_12_audit_complete(self) -> ComplianceCheckResult: + """Art. 12: Check if audit record is complete.""" + has_user = bool(self.data.user_id) + has_model = bool(self.data.model) + has_timestamp = bool(self.data.timestamp) + has_guardrails = len(self.guardrails) > 0 + audit_complete = has_user and has_model and has_timestamp and has_guardrails + + missing = [] + if not has_user: + missing.append("user_id") + if not has_model: + missing.append("model") + if not has_timestamp: + missing.append("timestamp") + if not has_guardrails: + missing.append("guardrail_results") + + return ComplianceCheckResult( + check_name="Audit record complete", + article="Art. 12", + passed=audit_complete, + detail=( + "All required audit fields present" + if audit_complete + else f"Missing: {', '.join(missing)}" + ), + ) + + # ── GDPR Helper Methods ────────────────────────────────────────────────── + + def _check_art_32_data_protection(self) -> ComplianceCheckResult: + """Art. 32: Check if data protection was applied (pre-call).""" + pre_call_guardrails = self._get_guardrails_by_mode("pre_call") + has_pre_call = len(pre_call_guardrails) > 0 + return ComplianceCheckResult( + check_name="Data protection applied", + article="Art. 32", + passed=has_pre_call, + detail=( + f"{len(pre_call_guardrails)} pre-call guardrail(s) protect data" + if has_pre_call + else "No pre-call data protection applied" + ), + ) + + def _check_art_5_1c_sensitive_data_protected(self) -> ComplianceCheckResult: + """Art. 5(1)(c): Check if sensitive data was protected.""" + pre_call_guardrails = self._get_guardrails_by_mode("pre_call") + has_intervention = self._has_guardrail_intervention(pre_call_guardrails) + all_passed = self._all_guardrails_passed(pre_call_guardrails) + data_protected = has_intervention or all_passed + + if has_intervention: + detail = "Guardrail intervened to protect sensitive data" + elif all_passed: + detail = "No sensitive data detected" + else: + detail = "No pre-call guardrails to protect sensitive data" + + return ComplianceCheckResult( + check_name="Sensitive data protected", + article="Art. 5(1)(c)", + passed=data_protected, + detail=detail, + ) + + def _check_art_30_audit_complete(self) -> ComplianceCheckResult: + """Art. 30: Check if audit record is complete.""" + has_user = bool(self.data.user_id) + has_model = bool(self.data.model) + has_timestamp = bool(self.data.timestamp) + has_guardrails = len(self.guardrails) > 0 + audit_complete = has_user and has_model and has_timestamp and has_guardrails + + missing = [] + if not has_user: + missing.append("user_id") + if not has_model: + missing.append("model") + if not has_timestamp: + missing.append("timestamp") + if not has_guardrails: + missing.append("guardrail_results") + + return ComplianceCheckResult( + check_name="Audit record complete", + article="Art. 30", + passed=audit_complete, + detail=( + "All required audit fields present" + if audit_complete + else f"Missing: {', '.join(missing)}" + ), + ) + + # ── Main Compliance Check Methods ──────────────────────────────────────── + + def check_eu_ai_act(self) -> List[ComplianceCheckResult]: + """ + Check EU AI Act compliance. + + Returns: + List of compliance check results for: + - Art. 9: Guardrails applied + - Art. 5: Content screened before LLM (pre-call screening) + - Art. 12: Audit record complete + """ + return [ + self._check_art_9_guardrails_applied(), + self._check_art_5_content_screened(), + self._check_art_12_audit_complete(), + ] + + def check_gdpr(self) -> List[ComplianceCheckResult]: + """ + Check GDPR compliance. + + Returns: + List of compliance check results for: + - Art. 32: Data protection applied (pre-call screening) + - Art. 5(1)(c): Sensitive data protected + - Art. 30: Audit record complete + """ + return [ + self._check_art_32_data_protection(), + self._check_art_5_1c_sensitive_data_protected(), + self._check_art_30_audit_complete(), + ] diff --git a/litellm/proxy/management_endpoints/compliance_endpoints.py b/litellm/proxy/management_endpoints/compliance_endpoints.py new file mode 100644 index 0000000000..4db99ecce1 --- /dev/null +++ b/litellm/proxy/management_endpoints/compliance_endpoints.py @@ -0,0 +1,79 @@ +""" +COMPLIANCE CHECK ENDPOINTS + +Endpoints for checking regulatory compliance of LLM request logs. + +/compliance/eu-ai-act - Check EU AI Act compliance +/compliance/gdpr - Check GDPR compliance +""" + +from fastapi import APIRouter, Depends, Request + +from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy.auth.user_api_key_auth import user_api_key_auth +from litellm.proxy.compliance_checks import ComplianceChecker +from litellm.proxy.management_helpers.utils import management_endpoint_wrapper +from litellm.types.proxy.compliance_endpoints import ( + ComplianceCheckRequest, + ComplianceResponse, +) + +router = APIRouter() + + +@router.post( + "/compliance/eu-ai-act", + tags=["compliance"], + dependencies=[Depends(user_api_key_auth)], + response_model=ComplianceResponse, +) +@management_endpoint_wrapper +async def check_eu_ai_act_compliance( + data: ComplianceCheckRequest, + http_request: Request, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +) -> ComplianceResponse: + """ + Check EU AI Act compliance for a spend log entry. + + Checks: + - Art. 9: Guardrails applied (any guardrail) + - Art. 5: Content screened before LLM (pre-call guardrails) + - Art. 12: Audit record complete (user_id, model, timestamp, guardrail_results) + """ + checker = ComplianceChecker(data) + checks = checker.check_eu_ai_act() + return ComplianceResponse( + compliant=all(c.passed for c in checks), + regulation="EU AI Act", + checks=checks, + ) + + +@router.post( + "/compliance/gdpr", + tags=["compliance"], + dependencies=[Depends(user_api_key_auth)], + response_model=ComplianceResponse, +) +@management_endpoint_wrapper +async def check_gdpr_compliance( + data: ComplianceCheckRequest, + http_request: Request, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +) -> ComplianceResponse: + """ + Check GDPR compliance for a spend log entry. + + Checks: + - Art. 32: Data protection applied (pre-call guardrails) + - Art. 5(1)(c): Sensitive data protected (masked/blocked or no issues) + - Art. 30: Audit record complete (user_id, model, timestamp, guardrail_results) + """ + checker = ComplianceChecker(data) + checks = checker.check_gdpr() + return ComplianceResponse( + compliant=all(c.passed for c in checks), + regulation="GDPR", + checks=checks, + ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index e332dd1763..7d1067f7b0 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -345,6 +345,9 @@ from litellm.proxy.management_endpoints.common_utils import ( _user_has_admin_privileges, admin_can_invite_user, ) +from litellm.proxy.management_endpoints.compliance_endpoints import ( + router as compliance_router, +) from litellm.proxy.management_endpoints.cost_tracking_settings import ( router as cost_tracking_settings_router, ) @@ -12467,6 +12470,7 @@ app.include_router(user_agent_analytics_router) app.include_router(enterprise_router) app.include_router(ui_discovery_endpoints_router) app.include_router(agent_endpoints_router) +app.include_router(compliance_router) app.include_router(a2a_router) app.include_router(access_group_router) ######################################################## diff --git a/litellm/types/proxy/compliance_endpoints.py b/litellm/types/proxy/compliance_endpoints.py new file mode 100644 index 0000000000..154c9f403a --- /dev/null +++ b/litellm/types/proxy/compliance_endpoints.py @@ -0,0 +1,33 @@ +from typing import List, Optional + +from pydantic import BaseModel + + +class ComplianceCheckResult(BaseModel): + """Result of a single compliance check.""" + + check_name: str + article: str + passed: bool + detail: str + + +class ComplianceResponse(BaseModel): + """Response from a compliance check endpoint.""" + + compliant: bool + regulation: str + checks: List[ComplianceCheckResult] + + +class ComplianceCheckRequest(BaseModel): + """Request payload for compliance check endpoints. + + Mirrors the spend log fields needed for compliance evaluation. + """ + + request_id: str + user_id: Optional[str] = None + model: Optional[str] = None + timestamp: Optional[str] = None + guardrail_information: Optional[List[dict]] = None diff --git a/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py new file mode 100644 index 0000000000..2c41b16ba7 --- /dev/null +++ b/tests/test_litellm/proxy/management_endpoints/test_compliance_endpoints.py @@ -0,0 +1,387 @@ +""" +Unit tests for compliance check endpoints (EU AI Act and GDPR). +""" + +import os +import sys + +import pytest + +sys.path.insert( + 0, os.path.abspath("../../../..") +) # Adds the parent directory to the system path + +from litellm.proxy.compliance_checks import ComplianceChecker +from litellm.types.proxy.compliance_endpoints import ComplianceCheckRequest + +# --------------------------------------------------------------------------- +# EU AI Act — Non-compliant cases (Task #3) +# --------------------------------------------------------------------------- + + +class TestEuAiActNonCompliant: + """Requests that should NOT be EU AI Act compliant.""" + + def test_no_guardrails_applied(self): + """Request with no guardrail information at all.""" + data = ComplianceCheckRequest( + request_id="req-001", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=None, + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Guardrails applied"] is False + assert results["Content screened before LLM"] is False + assert results["Audit record complete"] is False + + def test_empty_guardrails_list(self): + """Request with an empty guardrail list.""" + data = ComplianceCheckRequest( + request_id="req-002", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Guardrails applied"] is False + assert results["Content screened before LLM"] is False + assert results["Audit record complete"] is False + + def test_no_prohibited_practices_screening(self): + """Guardrails exist but only post-call (no pre-call screening).""" + data = ComplianceCheckRequest( + request_id="req-003", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "content_filter", + "guardrail_mode": "post_call", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Guardrails applied"] is True + assert results["Content screened before LLM"] is False + + def test_incomplete_audit_missing_user_id(self): + """Audit record missing user_id.""" + data = ComplianceCheckRequest( + request_id="req-004", + user_id=None, + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "prohibited_practices", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Audit record complete"] is False + + def test_incomplete_audit_missing_model(self): + """Audit record missing model.""" + data = ComplianceCheckRequest( + request_id="req-005", + user_id="user-1", + model=None, + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "prohibited_practices", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Audit record complete"] is False + + def test_incomplete_audit_missing_timestamp(self): + """Audit record missing timestamp.""" + data = ComplianceCheckRequest( + request_id="req-006", + user_id="user-1", + model="gpt-4", + timestamp=None, + guardrail_information=[ + { + "guardrail_name": "prohibited_practices", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Audit record complete"] is False + + def test_incomplete_audit_missing_guardrails(self): + """Audit record has user/model/timestamp but no guardrails.""" + data = ComplianceCheckRequest( + request_id="req-007", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Audit record complete"] is False + + +# --------------------------------------------------------------------------- +# GDPR — Non-compliant cases (Task #3) +# --------------------------------------------------------------------------- + + +class TestGdprNonCompliant: + """Requests that should NOT be GDPR compliant.""" + + def test_no_pii_detection(self): + """Guardrails exist but only post-call (no pre-call data protection).""" + data = ComplianceCheckRequest( + request_id="req-101", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "content_filter", + "guardrail_mode": "post_call", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Data protection applied"] is False + assert results["Sensitive data protected"] is False + + def test_empty_guardrails(self): + """Empty guardrail list — no PII scan.""" + data = ComplianceCheckRequest( + request_id="req-102", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Data protection applied"] is False + assert results["Audit record complete"] is False + + def test_pii_sent_in_plaintext(self): + """PII detection ran but status indicates PII was passed through.""" + data = ComplianceCheckRequest( + request_id="req-103", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "pii_detection", + "guardrail_status": "pii_detected_not_blocked", + } + ], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Data protection applied"] is True + assert results["Sensitive data protected"] is False + + def test_gdpr_audit_missing_user_id(self): + """GDPR audit missing user_id.""" + data = ComplianceCheckRequest( + request_id="req-104", + user_id=None, + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "pii_detection", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Audit record complete"] is False + + def test_gdpr_audit_missing_model(self): + """GDPR audit missing model.""" + data = ComplianceCheckRequest( + request_id="req-105", + user_id="user-1", + model=None, + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "pii_detection", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Audit record complete"] is False + + def test_no_guardrails_at_all(self): + """None guardrail_information.""" + data = ComplianceCheckRequest( + request_id="req-106", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=None, + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Data protection applied"] is False + assert results["Audit record complete"] is False + + +# --------------------------------------------------------------------------- +# EU AI Act — Compliant cases (Task #4) +# --------------------------------------------------------------------------- + + +class TestEuAiActCompliant: + """Requests that SHOULD be EU AI Act compliant.""" + + def test_fully_compliant(self): + """All checks pass: guardrails, prohibited_practices, full audit.""" + data = ComplianceCheckRequest( + request_id="req-201", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "content_filter", + "guardrail_status": "success", + }, + { + "guardrail_name": "prohibited_practices", + "guardrail_status": "success", + }, + ], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + results = {c.check_name: c.passed for c in checks} + assert results["Guardrails applied"] is True + assert results["Content screened before LLM"] is True + assert results["Audit record complete"] is True + assert all(c.passed for c in checks) + + def test_compliant_with_multiple_guardrails(self): + """Multiple guardrails including prohibited_practices.""" + data = ComplianceCheckRequest( + request_id="req-202", + user_id="user-2", + model="claude-3", + timestamp="2026-02-17T12:00:00Z", + guardrail_information=[ + { + "guardrail_name": "pii_detection", + "guardrail_status": "success", + }, + { + "guardrail_name": "prohibited_practices", + "guardrail_status": "success", + }, + { + "guardrail_name": "content_filter", + "guardrail_status": "success", + }, + ], + ) + checks = ComplianceChecker(data).check_eu_ai_act() + assert all(c.passed for c in checks) + + +# --------------------------------------------------------------------------- +# GDPR — Compliant cases (Task #4) +# --------------------------------------------------------------------------- + + +class TestGdprCompliant: + """Requests that SHOULD be GDPR compliant.""" + + def test_fully_compliant_pii_no_issues(self): + """PII scan ran, found nothing (status=success), full audit.""" + data = ComplianceCheckRequest( + request_id="req-301", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "pii_detection", + "guardrail_status": "success", + } + ], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Data protection applied"] is True + assert results["Sensitive data protected"] is True + assert results["Audit record complete"] is True + assert all(c.passed for c in checks) + + def test_compliant_pii_masked(self): + """PII detected and masked (guardrail_intervened) — still compliant.""" + data = ComplianceCheckRequest( + request_id="req-302", + user_id="user-1", + model="gpt-4", + timestamp="2026-02-17T00:00:00Z", + guardrail_information=[ + { + "guardrail_name": "pii_detection", + "guardrail_status": "guardrail_intervened", + } + ], + ) + checks = ComplianceChecker(data).check_gdpr() + results = {c.check_name: c.passed for c in checks} + assert results["Data protection applied"] is True + assert results["Sensitive data protected"] is True + assert results["Audit record complete"] is True + assert all(c.passed for c in checks) + + def test_compliant_with_other_guardrails(self): + """PII detection plus other guardrails — still compliant.""" + data = ComplianceCheckRequest( + request_id="req-303", + user_id="user-2", + model="claude-3", + timestamp="2026-02-17T12:00:00Z", + guardrail_information=[ + { + "guardrail_name": "content_filter", + "guardrail_status": "success", + }, + { + "guardrail_name": "pii_detection", + "guardrail_status": "success", + }, + { + "guardrail_name": "prohibited_practices", + "guardrail_status": "success", + }, + ], + ) + checks = ComplianceChecker(data).check_gdpr() + assert all(c.passed for c in checks) diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 35e713a94c..905514a69b 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -9235,3 +9235,70 @@ export const deleteClaudeCodePlugin = async (accessToken: string, pluginName: st throw error; } }; + +// Compliance check types and functions + +export interface ComplianceCheckResult { + check_name: string; + article: string; + passed: boolean; + detail: string; +} + +export interface ComplianceResponse { + compliant: boolean; + regulation: string; + checks: ComplianceCheckResult[]; +} + +export interface ComplianceCheckRequest { + request_id: string; + user_id?: string; + model?: string; + timestamp?: string; + guardrail_information?: Record[]; +} + +export const checkEuAiActCompliance = async ( + accessToken: string, + payload: ComplianceCheckRequest +): Promise => { + const url = proxyBaseUrl + ? `${proxyBaseUrl}/compliance/eu-ai-act` + : `/compliance/eu-ai-act`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + const errorData = await response.text(); + throw new Error(errorData); + } + return response.json(); +}; + +export const checkGdprCompliance = async ( + accessToken: string, + payload: ComplianceCheckRequest +): Promise => { + const url = proxyBaseUrl + ? `${proxyBaseUrl}/compliance/gdpr` + : `/compliance/gdpr`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + }); + if (!response.ok) { + const errorData = await response.text(); + throw new Error(errorData); + } + return response.json(); +}; diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx new file mode 100644 index 0000000000..cd36030e51 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/CompliancePanel.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect } from "react"; +import { Tooltip } from "antd"; +import { + checkEuAiActCompliance, + checkGdprCompliance, + ComplianceResponse, + ComplianceCheckRequest, +} from "@/components/networking"; + +interface CompliancePanelProps { + accessToken: string | null; + logEntry: { + request_id: string; + user?: string; + model?: string; + startTime?: string; + metadata?: Record; + }; +} + +// -- Icons -- + +const CheckIcon = () => ( + + + + +); + +const CrossIcon = () => ( + + + + +); + +const SpinnerIcon = () => ( + + + + +); + +// -- Sub-components -- + +const ComplianceCard = ({ + title, + data, + loading, + error, +}: { + title: string; + data: ComplianceResponse | null; + loading: boolean; + error: string | null; +}) => { + const [expanded, setExpanded] = useState(false); + + return ( +
+
setExpanded(!expanded)} + > +
+ {loading ? ( + + ) : error ? ( + + -- + + ) : data?.compliant ? ( + + ) : ( + + )} + {title} +
+
+ {!loading && !error && data && ( + + {data.compliant ? "COMPLIANT" : "NON-COMPLIANT"} + + )} + {error && ( + + UNAVAILABLE + + )} + + + +
+
+ + {expanded && ( +
+ {loading &&

Checking compliance...

} + {error &&

{error}

} + {data && ( +
+ {data.checks.map((check, idx) => ( +
+
+ {check.passed ? : } +
+
+
+ {check.check_name} + {check.article} +
+

{check.detail}

+
+
+ ))} +
+ )} +
+ )} +
+ ); +}; + +// -- 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]); + + return ( +
+

+ Regulatory Compliance +

+
+ + +
+
+ ); +}; + +export default CompliancePanel; 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 c916818a0e..12abf9c3c6 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx @@ -5,6 +5,7 @@ import BedrockGuardrailDetails, { BedrockGuardrailResponse, } from "@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails"; import ContentFilterDetails from "./ContentFilterDetails"; +import CompliancePanel from "./CompliancePanel"; // ── Interfaces ────────────────────────────────────────────────────────────── @@ -58,6 +59,14 @@ interface GuardrailInformation { interface GuardrailViewerProps { data: GuardrailInformation | GuardrailInformation[]; + accessToken?: string | null; + logEntry?: { + request_id: string; + user?: string; + model?: string; + startTime?: string; + metadata?: Record; + }; } // ── Helpers ───────────────────────────────────────────────────────────────── @@ -585,7 +594,7 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => { // ── Main Component ────────────────────────────────────────────────────────── -const GuardrailViewer = ({ data }: GuardrailViewerProps) => { +const GuardrailViewer = ({ data, accessToken, logEntry }: GuardrailViewerProps) => { const guardrailEntries = useMemo(() => { return Array.isArray(data) ? data.filter((entry): entry is GuardrailInformation => Boolean(entry)) @@ -659,11 +668,6 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
Total: {totalOverheadMs}ms overhead
- {policyTemplates.length > 0 && ( -
- Policy: {policyTemplates.join(" / ")} -
- )}