diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx
index 8d64d727189..e00f27c39f4 100644
--- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx
+++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.test.tsx
@@ -271,6 +271,70 @@ describe("ChatUI", () => {
});
});
+ it("should show Simulate failure to test fallbacks in Model Settings when chat endpoint is selected", async () => {
+ render(
+
,
+ );
+
+ await waitFor(() => {
+ expect(screen.getByText("Test Key")).toBeInTheDocument();
+ });
+
+ // Model Settings button only appears when a chat model is selected; select "Model 1" first
+ const selectModelLabel = screen.getByText("Select Model");
+ const modelSelectContainer = selectModelLabel.closest("div");
+ const modelSelect = modelSelectContainer?.querySelector(".ant-select-selector");
+ expect(modelSelect).toBeTruthy();
+
+ await act(async () => {
+ fireEvent.mouseDown(modelSelect!);
+ });
+
+ await waitFor(() => {
+ expect(screen.getAllByText("Model 1").length).toBeGreaterThan(0);
+ });
+
+ // Ant Design Select options may not have role="option"; click the dropdown option by text
+ const model1Options = screen.getAllByText("Model 1");
+ await act(async () => {
+ fireEvent.click(model1Options[model1Options.length - 1]);
+ });
+
+ await waitFor(() => {
+ const modelSettingsButton = screen.getByTestId("model-settings-button");
+ expect(modelSettingsButton).toBeInTheDocument();
+ });
+
+ const modelSettingsButton = screen.getByTestId("model-settings-button");
+ await act(async () => {
+ fireEvent.click(modelSettingsButton);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByText("Model Settings")).toBeInTheDocument();
+ expect(screen.getByText(/Simulate failure to test fallbacks/i)).toBeInTheDocument();
+ });
+
+ const fallbacksCheckbox = screen.getByRole("checkbox", {
+ name: /Simulate failure to test fallbacks/i,
+ });
+ expect(fallbacksCheckbox).not.toBeChecked();
+
+ await act(async () => {
+ fireEvent.click(fallbacksCheckbox);
+ });
+
+ await waitFor(() => {
+ expect(screen.getByRole("checkbox", { name: /Simulate failure to test fallbacks/i })).toBeChecked();
+ });
+ });
+
it("should show Fill button and populate customProxyBaseUrl when proxySettings.LITELLM_UI_API_DOC_BASE_URL is provided", async () => {
const testProxyUrl = "http://localhost:5000";
diff --git a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx
index 92898c26bf2..7e50ca6b95a 100644
--- a/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx
+++ b/ui/litellm-dashboard/src/components/playground/chat_ui/ChatUI.tsx
@@ -229,6 +229,7 @@ const ChatUI: React.FC
= ({
const [temperature, setTemperature] = useState(1.0);
const [maxTokens, setMaxTokens] = useState(2048);
const [useAdvancedParams, setUseAdvancedParams] = useState(false);
+ const [mockTestFallbacks, setMockTestFallbacks] = useState(false);
// Code Interpreter state (using custom hook)
const codeInterpreter = useCodeInterpreter();
@@ -982,6 +983,7 @@ const ChatUI: React.FC = ({
mcpServers,
mcpServerToolRestrictions,
handleMCPEvent,
+ mockTestFallbacks,
);
} else if (endpointType === EndpointType.IMAGE) {
// For image generation
@@ -1401,6 +1403,8 @@ const ChatUI: React.FC = ({
onTemperatureChange={setTemperature}
onMaxTokensChange={setMaxTokens}
onUseAdvancedParamsChange={setUseAdvancedParams}
+ mockTestFallbacks={mockTestFallbacks}
+ onMockTestFallbacksChange={setMockTestFallbacks}
/>
}
title="Model Settings"
@@ -1412,6 +1416,8 @@ const ChatUI: React.FC = ({
size="small"
icon={}
className="text-gray-500 hover:text-gray-700"
+ aria-label="Model Settings"
+ data-testid="model-settings-button"
/>
) : (
@@ -2390,7 +2396,7 @@ const ChatUI: React.FC = ({
setIsGetCodeModalVisible(false)}
footer={null}
width={800}
diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx
index 8786e022013..8649834b318 100644
--- a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx
+++ b/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.test.tsx
@@ -190,4 +190,70 @@ describe("chat_completion", () => {
expect(secondTool.require_approval).toBe("never");
expect(secondTool.allowed_tools).toEqual(["toolC"]);
});
+
+ it("should include mock_testing_fallbacks in request body when mockTestFallbacks is true", async () => {
+ await makeOpenAIChatCompletionRequest(
+ mockChatHistory,
+ mockUpdateUI,
+ "gpt-4",
+ "test-token",
+ undefined, // tags
+ undefined, // signal
+ undefined, // onReasoningContent
+ undefined, // onTimingData
+ undefined, // onUsageData
+ undefined, // traceId
+ undefined, // vector_store_ids
+ undefined, // guardrails
+ undefined, // policies
+ undefined, // selectedMCPServers
+ undefined, // onImageGenerated
+ undefined, // onSearchResults
+ undefined, // temperature
+ undefined, // max_tokens
+ undefined, // onTotalLatency
+ undefined, // customBaseUrl
+ undefined, // mcpServers
+ undefined, // mcpServerToolRestrictions
+ undefined, // onMCPEvent
+ true, // mockTestFallbacks
+ );
+
+ expect(mockCreate).toHaveBeenCalledTimes(1);
+ const callArgs = mockCreate.mock.calls[0][0];
+ expect(callArgs.mock_testing_fallbacks).toBe(true);
+ });
+
+ it("should not include mock_testing_fallbacks in request body when mockTestFallbacks is false or undefined", async () => {
+ await makeOpenAIChatCompletionRequest(
+ mockChatHistory,
+ mockUpdateUI,
+ "gpt-4",
+ "test-token",
+ undefined, // tags
+ undefined, // signal
+ undefined, // onReasoningContent
+ undefined, // onTimingData
+ undefined, // onUsageData
+ undefined, // traceId
+ undefined, // vector_store_ids
+ undefined, // guardrails
+ undefined, // policies
+ undefined, // selectedMCPServers
+ undefined, // onImageGenerated
+ undefined, // onSearchResults
+ undefined, // temperature
+ undefined, // max_tokens
+ undefined, // onTotalLatency
+ undefined, // customBaseUrl
+ undefined, // mcpServers
+ undefined, // mcpServerToolRestrictions
+ undefined, // onMCPEvent
+ false, // mockTestFallbacks
+ );
+
+ expect(mockCreate).toHaveBeenCalledTimes(1);
+ const callArgs = mockCreate.mock.calls[0][0];
+ expect(callArgs).not.toHaveProperty("mock_testing_fallbacks");
+ });
});
diff --git a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx b/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx
index 61d232082e0..048ea9bfa11 100644
--- a/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx
+++ b/ui/litellm-dashboard/src/components/playground/llm_calls/chat_completion.tsx
@@ -30,6 +30,7 @@ export async function makeOpenAIChatCompletionRequest(
mcpServers?: MCPServer[],
mcpServerToolRestrictions?: Record,
onMCPEvent?: (event: MCPEvent) => void,
+ mockTestFallbacks?: boolean,
) {
// base url should be the current base_url
const isLocal = process.env.NODE_ENV === "development";
@@ -115,6 +116,7 @@ export async function makeOpenAIChatCompletionRequest(
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
...(temperature !== undefined ? { temperature } : {}),
...(max_tokens !== undefined ? { max_tokens } : {}),
+ ...(mockTestFallbacks ? { mock_testing_fallbacks: true } : {}),
},
{ signal },
);
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 ed2198ba859..8f9869e4dd5 100644
--- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx
@@ -24,6 +24,15 @@ interface MaskedEntityCount {
[key: string]: number;
}
+interface MatchDetail {
+ type: string;
+ detection_method?: string;
+ action_taken?: string;
+ snippet?: string;
+ category?: string;
+ position?: number;
+}
+
interface GuardrailInformation {
duration: number;
end_time: number;
@@ -33,7 +42,15 @@ interface GuardrailInformation {
guardrail_status: string;
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse | any;
masked_entity_count: MaskedEntityCount;
- guardrail_provider?: string; // "presidio" | "bedrock" | "litellm_content_filter" | other providers
+ guardrail_provider?: string;
+ guardrail_id?: string;
+ policy_template?: string;
+ detection_method?: string;
+ confidence_score?: number;
+ classification?: Record;
+ match_details?: MatchDetail[];
+ patterns_checked?: number;
+ alert_recipients?: string[];
}
interface GuardrailViewerProps {
@@ -87,6 +104,179 @@ 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;
+
+ 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}
+
+ )}
+
+
+ );
+};
+
+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 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}
+
+ )}
+
+
+ );
+ })}
+
+
+ );
+};
+
const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
const guardrailProvider = entry.guardrail_provider ?? "presidio";
const statusLabel = entry.guardrail_status ?? "unknown";
@@ -127,6 +317,12 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
Guardrail Name:
{entry.guardrail_name}
+ {entry.guardrail_id && entry.guardrail_id !== entry.guardrail_name && (
+
+ Guardrail ID:
+ {entry.guardrail_id}
+
+ )}
Mode:
{entry.guardrail_mode}
@@ -161,6 +357,17 @@ const GuardrailDetails = ({ entry, index, total }: GuardrailDetailsProps) => {
+ {/* Policy, detection method, confidence, patterns checked */}
+
+
+ {/* Classification details (LLM-judge) */}
+ {entry.classification &&
}
+
+ {/* Match details table */}
+ {entry.match_details && entry.match_details.length > 0 && (
+
+ )}
+
{totalMaskedEntities > 0 && (
Masked Entity Summary
@@ -222,6 +429,10 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
);
}, 0);
+ const policyTemplates = Array.from(
+ new Set(guardrailEntries.map((e) => e.policy_template).filter(Boolean))
+ );
+
const tooltipTitle = allSucceeded ? null : "Guardrail failed to run.";
if (guardrailEntries.length === 0) {
@@ -237,7 +448,7 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
{
key: "1",
label: (
-
+
Guardrail Information
@@ -257,10 +468,17 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => {
{totalMaskedEntities} masked {totalMaskedEntities === 1 ? "entity" : "entities"}
)}
+
+ {policyTemplates.map((pt) => (
+
+ {pt}
+
+ ))}
),
children: (
+
{guardrailEntries.map((entry, index) => (