From 5a3acb43e4f2eb32d91dc6bd0d737e4fe6911d98 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 10 Feb 2026 16:29:10 -0800 Subject: [PATCH] ui: add policy test --- .../components/policies/policy_test_panel.tsx | 254 ++++++++++++++++++ .../src/components/policies/types.ts | 2 + 2 files changed, 256 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/policies/policy_test_panel.tsx diff --git a/ui/litellm-dashboard/src/components/policies/policy_test_panel.tsx b/ui/litellm-dashboard/src/components/policies/policy_test_panel.tsx new file mode 100644 index 00000000000..055c206ae44 --- /dev/null +++ b/ui/litellm-dashboard/src/components/policies/policy_test_panel.tsx @@ -0,0 +1,254 @@ +import React, { useState, useEffect } from "react"; +import { Form, Select, Alert, Tag, Empty, Typography } from "antd"; +import { Button } from "@tremor/react"; +import { resolvePoliciesCall, teamListCall, keyListCall, modelAvailableCall } from "../networking"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +const { Text } = Typography; + +interface PolicyTestPanelProps { + accessToken: string | null; +} + +interface PolicyMatchDetail { + policy_name: string; + matched_via: string; + guardrails_added: string[]; +} + +interface ResolveResult { + effective_guardrails: string[]; + matched_policies: PolicyMatchDetail[]; +} + +const PolicyTestPanel: React.FC = ({ accessToken }) => { + const [form] = Form.useForm(); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + const [hasSearched, setHasSearched] = useState(false); + const [availableTeams, setAvailableTeams] = useState([]); + const [availableKeys, setAvailableKeys] = useState([]); + const [availableModels, setAvailableModels] = useState([]); + const { userId, userRole } = useAuthorized(); + + useEffect(() => { + if (accessToken) { + loadOptions(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [accessToken]); + + const loadOptions = async () => { + if (!accessToken) return; + + try { + const teamsResponse = await teamListCall(accessToken, null, userId); + const teamsArray = Array.isArray(teamsResponse) ? teamsResponse : (teamsResponse?.data || []); + setAvailableTeams( + teamsArray.map((t: any) => t.team_alias).filter(Boolean) + ); + } catch (error) { + console.error("Failed to load teams:", error); + } + + try { + const keysResponse = await keyListCall(accessToken, null, null, null, null, null, 1, 100); + const keysArray = keysResponse?.keys || keysResponse?.data || []; + setAvailableKeys( + keysArray.map((k: any) => k.key_alias).filter(Boolean) + ); + } catch (error) { + console.error("Failed to load keys:", error); + } + + try { + const modelsResponse = await modelAvailableCall(accessToken, userId || "", userRole || ""); + const modelsArray = modelsResponse?.data || (Array.isArray(modelsResponse) ? modelsResponse : []); + setAvailableModels( + modelsArray.map((m: any) => m.id || m.model_name).filter(Boolean) + ); + } catch (error) { + console.error("Failed to load models:", error); + } + }; + + const handleTest = async () => { + if (!accessToken) return; + + setIsLoading(true); + setHasSearched(true); + try { + const values = form.getFieldsValue(true); + const context: any = {}; + if (values.team_alias) context.team_alias = values.team_alias; + if (values.key_alias) context.key_alias = values.key_alias; + if (values.model) context.model = values.model; + if (values.tags && values.tags.length > 0) context.tags = values.tags; + + const data = await resolvePoliciesCall(accessToken, context); + setResult(data); + } catch (error) { + console.error("Error resolving policies:", error); + setResult(null); + } finally { + setIsLoading(false); + } + }; + + const handleReset = () => { + form.resetFields(); + setResult(null); + setHasSearched(false); + }; + + return ( +
+
+
+

Policy Simulator

+ + Simulate a request to see which policies and guardrails would apply. Select a team, key, model, or tags below and click "Simulate" to see the results. + +
+ +
+
+ + ({ label: k, value: k }))} + filterOption={(input, option) => + (option?.label ?? "").toLowerCase().includes(input.toLowerCase()) + } + /> + + + + +
+
+ + +
+
+
+ + {!hasSearched && ( +
+
+ + + +
+

No simulation run yet

+

+ Fill in one or more fields above and click "Simulate" to see which policies and guardrails would apply to that request. +

+
+ )} + + {hasSearched && result && ( +
+ {result.matched_policies.length === 0 ? ( + + ) : ( + <> +
+

Effective Guardrails

+
+ {result.effective_guardrails.length > 0 ? ( + result.effective_guardrails.map((g) => ( + {g} + )) + ) : ( + None + )} +
+
+ +
+

Matched Policies

+ + + + + + + + + + {result.matched_policies.map((p) => ( + + + + + + ))} + +
PolicyMatched ViaGuardrails Added
{p.policy_name} + {p.matched_via} + + {p.guardrails_added.length > 0 ? ( +
+ {p.guardrails_added.map((g) => ( + {g} + ))} +
+ ) : ( + None + )} +
+
+ + )} +
+ )} + + {hasSearched && !result && !isLoading && ( + + )} +
+ ); +}; + +export default PolicyTestPanel; diff --git a/ui/litellm-dashboard/src/components/policies/types.ts b/ui/litellm-dashboard/src/components/policies/types.ts index 2430180b9db..3e2c6f1966d 100644 --- a/ui/litellm-dashboard/src/components/policies/types.ts +++ b/ui/litellm-dashboard/src/components/policies/types.ts @@ -23,6 +23,7 @@ export interface PolicyAttachment { teams: string[]; keys: string[]; models: string[]; + tags: string[]; created_at?: string; updated_at?: string; created_by?: string; @@ -53,6 +54,7 @@ export interface PolicyAttachmentCreateRequest { teams?: string[]; keys?: string[]; models?: string[]; + tags?: string[]; } export interface PolicyListResponse {