From c68ee52a4d2cb9a5c6083c2d7f9c88a432b1629e Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Sat, 21 Feb 2026 16:49:28 -0800 Subject: [PATCH] fix: multiple QA fixes for policy flow builder with guardrail versioning on litellm --- .../out/{404.html => 404/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{budgets.html => budgets/index.html} | 0 .../{caching.html => caching/index.html} | 0 .../index.html} | 0 .../{old-usage.html => old-usage/index.html} | 0 .../{prompts.html => prompts/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../out/{login.html => login/index.html} | 0 .../out/{logs.html => logs/index.html} | 0 .../{callback.html => callback/index.html} | 0 .../{model-hub.html => model-hub/index.html} | 0 .../{model_hub.html => model_hub/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{policies.html => policies/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../{ui-theme.html => ui-theme/index.html} | 0 .../out/{teams.html => teams/index.html} | 0 .../{test-key.html => test-key/index.html} | 0 .../index.html} | 0 .../index.html} | 0 .../out/{usage.html => usage/index.html} | 0 .../out/{users.html => users/index.html} | 0 .../index.html} | 0 .../src/components/networking.tsx | 96 ++++ .../src/components/policies/index.tsx | 17 +- .../policies/pipeline_flow_builder.tsx | 533 +++++++++++++++++- .../src/components/policies/policy_table.tsx | 110 ++-- .../src/components/policies/types.ts | 9 + .../src/data/compliancePrompts.ts | 7 + ui/litellm-dashboard/tsconfig.json | 2 +- 40 files changed, 718 insertions(+), 56 deletions(-) rename litellm/proxy/_experimental/out/{404.html => 404/index.html} (100%) rename litellm/proxy/_experimental/out/{_not-found.html => _not-found/index.html} (100%) rename litellm/proxy/_experimental/out/{api-reference.html => api-reference/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{api-playground.html => api-playground/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{budgets.html => budgets/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{caching.html => caching/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{claude-code-plugins.html => claude-code-plugins/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{old-usage.html => old-usage/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{prompts.html => prompts/index.html} (100%) rename litellm/proxy/_experimental/out/experimental/{tag-management.html => tag-management/index.html} (100%) rename litellm/proxy/_experimental/out/{guardrails.html => guardrails/index.html} (100%) rename litellm/proxy/_experimental/out/{login.html => login/index.html} (100%) rename litellm/proxy/_experimental/out/{logs.html => logs/index.html} (100%) rename litellm/proxy/_experimental/out/mcp/oauth/{callback.html => callback/index.html} (100%) rename litellm/proxy/_experimental/out/{model-hub.html => model-hub/index.html} (100%) rename litellm/proxy/_experimental/out/{model_hub.html => model_hub/index.html} (100%) rename litellm/proxy/_experimental/out/{model_hub_table.html => model_hub_table/index.html} (100%) rename litellm/proxy/_experimental/out/{models-and-endpoints.html => models-and-endpoints/index.html} (100%) rename litellm/proxy/_experimental/out/{onboarding.html => onboarding/index.html} (100%) rename litellm/proxy/_experimental/out/{organizations.html => organizations/index.html} (100%) rename litellm/proxy/_experimental/out/{playground.html => playground/index.html} (100%) rename litellm/proxy/_experimental/out/{policies.html => policies/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{admin-settings.html => admin-settings/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{logging-and-alerts.html => logging-and-alerts/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{router-settings.html => router-settings/index.html} (100%) rename litellm/proxy/_experimental/out/settings/{ui-theme.html => ui-theme/index.html} (100%) rename litellm/proxy/_experimental/out/{teams.html => teams/index.html} (100%) rename litellm/proxy/_experimental/out/{test-key.html => test-key/index.html} (100%) rename litellm/proxy/_experimental/out/tools/{mcp-servers.html => mcp-servers/index.html} (100%) rename litellm/proxy/_experimental/out/tools/{vector-stores.html => vector-stores/index.html} (100%) rename litellm/proxy/_experimental/out/{usage.html => usage/index.html} (100%) rename litellm/proxy/_experimental/out/{users.html => users/index.html} (100%) rename litellm/proxy/_experimental/out/{virtual-keys.html => virtual-keys/index.html} (100%) diff --git a/litellm/proxy/_experimental/out/404.html b/litellm/proxy/_experimental/out/404/index.html similarity index 100% rename from litellm/proxy/_experimental/out/404.html rename to litellm/proxy/_experimental/out/404/index.html diff --git a/litellm/proxy/_experimental/out/_not-found.html b/litellm/proxy/_experimental/out/_not-found/index.html similarity index 100% rename from litellm/proxy/_experimental/out/_not-found.html rename to litellm/proxy/_experimental/out/_not-found/index.html diff --git a/litellm/proxy/_experimental/out/api-reference.html b/litellm/proxy/_experimental/out/api-reference/index.html similarity index 100% rename from litellm/proxy/_experimental/out/api-reference.html rename to litellm/proxy/_experimental/out/api-reference/index.html diff --git a/litellm/proxy/_experimental/out/experimental/api-playground.html b/litellm/proxy/_experimental/out/experimental/api-playground/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/api-playground.html rename to litellm/proxy/_experimental/out/experimental/api-playground/index.html diff --git a/litellm/proxy/_experimental/out/experimental/budgets.html b/litellm/proxy/_experimental/out/experimental/budgets/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/budgets.html rename to litellm/proxy/_experimental/out/experimental/budgets/index.html diff --git a/litellm/proxy/_experimental/out/experimental/caching.html b/litellm/proxy/_experimental/out/experimental/caching/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/caching.html rename to litellm/proxy/_experimental/out/experimental/caching/index.html diff --git a/litellm/proxy/_experimental/out/experimental/claude-code-plugins.html b/litellm/proxy/_experimental/out/experimental/claude-code-plugins/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/claude-code-plugins.html rename to litellm/proxy/_experimental/out/experimental/claude-code-plugins/index.html diff --git a/litellm/proxy/_experimental/out/experimental/old-usage.html b/litellm/proxy/_experimental/out/experimental/old-usage/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/old-usage.html rename to litellm/proxy/_experimental/out/experimental/old-usage/index.html diff --git a/litellm/proxy/_experimental/out/experimental/prompts.html b/litellm/proxy/_experimental/out/experimental/prompts/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/prompts.html rename to litellm/proxy/_experimental/out/experimental/prompts/index.html diff --git a/litellm/proxy/_experimental/out/experimental/tag-management.html b/litellm/proxy/_experimental/out/experimental/tag-management/index.html similarity index 100% rename from litellm/proxy/_experimental/out/experimental/tag-management.html rename to litellm/proxy/_experimental/out/experimental/tag-management/index.html diff --git a/litellm/proxy/_experimental/out/guardrails.html b/litellm/proxy/_experimental/out/guardrails/index.html similarity index 100% rename from litellm/proxy/_experimental/out/guardrails.html rename to litellm/proxy/_experimental/out/guardrails/index.html diff --git a/litellm/proxy/_experimental/out/login.html b/litellm/proxy/_experimental/out/login/index.html similarity index 100% rename from litellm/proxy/_experimental/out/login.html rename to litellm/proxy/_experimental/out/login/index.html diff --git a/litellm/proxy/_experimental/out/logs.html b/litellm/proxy/_experimental/out/logs/index.html similarity index 100% rename from litellm/proxy/_experimental/out/logs.html rename to litellm/proxy/_experimental/out/logs/index.html diff --git a/litellm/proxy/_experimental/out/mcp/oauth/callback.html b/litellm/proxy/_experimental/out/mcp/oauth/callback/index.html similarity index 100% rename from litellm/proxy/_experimental/out/mcp/oauth/callback.html rename to litellm/proxy/_experimental/out/mcp/oauth/callback/index.html diff --git a/litellm/proxy/_experimental/out/model-hub.html b/litellm/proxy/_experimental/out/model-hub/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model-hub.html rename to litellm/proxy/_experimental/out/model-hub/index.html diff --git a/litellm/proxy/_experimental/out/model_hub.html b/litellm/proxy/_experimental/out/model_hub/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub.html rename to litellm/proxy/_experimental/out/model_hub/index.html diff --git a/litellm/proxy/_experimental/out/model_hub_table.html b/litellm/proxy/_experimental/out/model_hub_table/index.html similarity index 100% rename from litellm/proxy/_experimental/out/model_hub_table.html rename to litellm/proxy/_experimental/out/model_hub_table/index.html diff --git a/litellm/proxy/_experimental/out/models-and-endpoints.html b/litellm/proxy/_experimental/out/models-and-endpoints/index.html similarity index 100% rename from litellm/proxy/_experimental/out/models-and-endpoints.html rename to litellm/proxy/_experimental/out/models-and-endpoints/index.html diff --git a/litellm/proxy/_experimental/out/onboarding.html b/litellm/proxy/_experimental/out/onboarding/index.html similarity index 100% rename from litellm/proxy/_experimental/out/onboarding.html rename to litellm/proxy/_experimental/out/onboarding/index.html diff --git a/litellm/proxy/_experimental/out/organizations.html b/litellm/proxy/_experimental/out/organizations/index.html similarity index 100% rename from litellm/proxy/_experimental/out/organizations.html rename to litellm/proxy/_experimental/out/organizations/index.html diff --git a/litellm/proxy/_experimental/out/playground.html b/litellm/proxy/_experimental/out/playground/index.html similarity index 100% rename from litellm/proxy/_experimental/out/playground.html rename to litellm/proxy/_experimental/out/playground/index.html diff --git a/litellm/proxy/_experimental/out/policies.html b/litellm/proxy/_experimental/out/policies/index.html similarity index 100% rename from litellm/proxy/_experimental/out/policies.html rename to litellm/proxy/_experimental/out/policies/index.html diff --git a/litellm/proxy/_experimental/out/settings/admin-settings.html b/litellm/proxy/_experimental/out/settings/admin-settings/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/admin-settings.html rename to litellm/proxy/_experimental/out/settings/admin-settings/index.html diff --git a/litellm/proxy/_experimental/out/settings/logging-and-alerts.html b/litellm/proxy/_experimental/out/settings/logging-and-alerts/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/logging-and-alerts.html rename to litellm/proxy/_experimental/out/settings/logging-and-alerts/index.html diff --git a/litellm/proxy/_experimental/out/settings/router-settings.html b/litellm/proxy/_experimental/out/settings/router-settings/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/router-settings.html rename to litellm/proxy/_experimental/out/settings/router-settings/index.html diff --git a/litellm/proxy/_experimental/out/settings/ui-theme.html b/litellm/proxy/_experimental/out/settings/ui-theme/index.html similarity index 100% rename from litellm/proxy/_experimental/out/settings/ui-theme.html rename to litellm/proxy/_experimental/out/settings/ui-theme/index.html diff --git a/litellm/proxy/_experimental/out/teams.html b/litellm/proxy/_experimental/out/teams/index.html similarity index 100% rename from litellm/proxy/_experimental/out/teams.html rename to litellm/proxy/_experimental/out/teams/index.html diff --git a/litellm/proxy/_experimental/out/test-key.html b/litellm/proxy/_experimental/out/test-key/index.html similarity index 100% rename from litellm/proxy/_experimental/out/test-key.html rename to litellm/proxy/_experimental/out/test-key/index.html diff --git a/litellm/proxy/_experimental/out/tools/mcp-servers.html b/litellm/proxy/_experimental/out/tools/mcp-servers/index.html similarity index 100% rename from litellm/proxy/_experimental/out/tools/mcp-servers.html rename to litellm/proxy/_experimental/out/tools/mcp-servers/index.html diff --git a/litellm/proxy/_experimental/out/tools/vector-stores.html b/litellm/proxy/_experimental/out/tools/vector-stores/index.html similarity index 100% rename from litellm/proxy/_experimental/out/tools/vector-stores.html rename to litellm/proxy/_experimental/out/tools/vector-stores/index.html diff --git a/litellm/proxy/_experimental/out/usage.html b/litellm/proxy/_experimental/out/usage/index.html similarity index 100% rename from litellm/proxy/_experimental/out/usage.html rename to litellm/proxy/_experimental/out/usage/index.html diff --git a/litellm/proxy/_experimental/out/users.html b/litellm/proxy/_experimental/out/users/index.html similarity index 100% rename from litellm/proxy/_experimental/out/users.html rename to litellm/proxy/_experimental/out/users/index.html diff --git a/litellm/proxy/_experimental/out/virtual-keys.html b/litellm/proxy/_experimental/out/virtual-keys/index.html similarity index 100% rename from litellm/proxy/_experimental/out/virtual-keys.html rename to litellm/proxy/_experimental/out/virtual-keys/index.html diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index a16afe9c8a2..a618988fa1a 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -5812,6 +5812,102 @@ export const updatePolicyCall = async (accessToken: string, policyId: string, po } }; +export const listPolicyVersions = async ( + accessToken: string, + policyName: string +): Promise<{ policy_name: string; versions: any[]; total_count: number }> => { + try { + const encodedName = encodeURIComponent(policyName); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/policies/name/${encodedName}/versions` + : `/policies/name/${encodedName}/versions`; + const response = await fetch(url, { + method: "GET", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + console.error("Failed to list policy versions:", error); + throw error; + } +}; + +export const createPolicyVersion = async ( + accessToken: string, + policyName: string, + sourcePolicyId?: string | null +): Promise => { + try { + const encodedName = encodeURIComponent(policyName); + const url = proxyBaseUrl + ? `${proxyBaseUrl}/policies/name/${encodedName}/versions` + : `/policies/name/${encodedName}/versions`; + const response = await fetch(url, { + method: "POST", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ source_policy_id: sourcePolicyId ?? undefined }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + console.error("Failed to create policy version:", error); + throw error; + } +}; + +export const updatePolicyVersionStatus = async ( + accessToken: string, + policyId: string, + versionStatus: "published" | "production" +): Promise => { + try { + const url = proxyBaseUrl + ? `${proxyBaseUrl}/policies/${policyId}/status` + : `/policies/${policyId}/status`; + const response = await fetch(url, { + method: "PUT", + headers: { + [globalLitellmHeaderName]: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ version_status: versionStatus }), + }); + + if (!response.ok) { + const errorData = await response.json(); + const errorMessage = deriveErrorMessage(errorData); + handleError(errorMessage); + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + console.error("Failed to update policy version status:", error); + throw error; + } +}; + export const deletePolicyCall = async (accessToken: string, policyId: string) => { try { const url = proxyBaseUrl ? `${proxyBaseUrl}/policies/${policyId}` : `/policies/${policyId}`; diff --git a/ui/litellm-dashboard/src/components/policies/index.tsx b/ui/litellm-dashboard/src/components/policies/index.tsx index 9ca382d3432..9c636d432da 100644 --- a/ui/litellm-dashboard/src/components/policies/index.tsx +++ b/ui/litellm-dashboard/src/components/policies/index.tsx @@ -469,11 +469,7 @@ const PoliciesPanel: React.FC = ({ onDeleteClick={handleDeleteClick} onEditClick={(policy) => { setEditingPolicy(policy); - if (policy.pipeline) { - setShowFlowBuilder(true); - } else { - setIsAddPolicyModalVisible(true); - } + setShowFlowBuilder(true); }} onViewClick={(policyId) => setSelectedPolicyId(policyId)} isAdmin={isAdmin} @@ -643,6 +639,17 @@ const PoliciesPanel: React.FC = ({ availableGuardrails={guardrailsList} createPolicy={createPolicyCall} updatePolicy={updatePolicyCall} + onVersionCreated={(newPolicy) => { + setEditingPolicy(newPolicy); + fetchPolicies(); + }} + onSelectVersion={(policy) => { + setEditingPolicy(policy); + }} + onVersionStatusUpdated={(updatedPolicy) => { + setEditingPolicy(updatedPolicy); + fetchPolicies(); + }} /> )} diff --git a/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx b/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx index 259378f2e19..22c45b0fb6a 100644 --- a/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx +++ b/ui/litellm-dashboard/src/components/policies/pipeline_flow_builder.tsx @@ -1,12 +1,14 @@ import React, { useState } from "react"; -import { Select, Typography, message } from "antd"; +import { Select, Typography, message, Spin } from "antd"; import { Button, TextInput } from "@tremor/react"; import { ArrowLeftIcon, PlusIcon } from "@heroicons/react/outline"; import { DotsVerticalIcon } from "@heroicons/react/solid"; import { GuardrailPipeline, PipelineStep, PipelineTestResult, PolicyCreateRequest, PolicyUpdateRequest, Policy } from "./types"; import { Guardrail } from "../guardrails/types"; -import { testPipelineCall } from "../networking"; +import { testPipelineCall, listPolicyVersions, createPolicyVersion, updatePolicyVersionStatus } from "../networking"; import NotificationsManager from "../molecules/notifications_manager"; +import { getComplianceDatasetPrompts } from "../../data/compliancePrompts"; +import type { CompliancePrompt } from "../../data/compliancePrompts"; const { Text } = Typography; @@ -55,6 +57,34 @@ function updateStepAtIndex( return steps.map((s, i) => (i === index ? { ...s, ...updated } : s)); } +/** + * Derives a pipeline from a policy. When the policy has a pipeline, use it. + * When it only has guardrails_add (legacy/simple form), convert those guardrails + * into pipeline steps in order. + */ +function derivePipelineFromPolicy(policy: Policy | null | undefined): GuardrailPipeline { + if (!policy) { + return { mode: "pre_call", steps: [createDefaultStep()] }; + } + if (policy.pipeline?.steps?.length) { + return policy.pipeline; + } + const guardrails = policy.guardrails_add || []; + if (guardrails.length > 0) { + return { + mode: policy.pipeline?.mode ?? "pre_call", + steps: guardrails.map((g) => ({ + guardrail: g, + on_pass: "next" as const, + on_fail: "block" as const, + pass_data: false, + modify_response_message: null, + })), + }; + } + return { mode: "pre_call", steps: [createDefaultStep()] }; +} + // ───────────────────────────────────────────────────────────────────────────── // Icons (matching the reference image) // ───────────────────────────────────────────────────────────────────────────── @@ -559,6 +589,20 @@ const TERMINAL_STYLES: Record = { modify_response: { bg: "#eff6ff", color: "#2563eb" }, }; +interface ComplianceRunEntry { + prompt: CompliancePrompt; + result: PipelineTestResult | null; + error?: string; + matched: boolean; +} + +function complianceMatchExpected(expected: "pass" | "fail", terminalAction: string): boolean { + if (expected === "pass") { + return terminalAction === "allow" || terminalAction === "modify_response"; + } + return terminalAction === "block"; +} + const PipelineTestPanel: React.FC = ({ pipeline, accessToken, @@ -568,6 +612,8 @@ const PipelineTestPanel: React.FC = ({ const [isRunning, setIsRunning] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); + const [complianceRunning, setComplianceRunning] = useState(false); + const [complianceResults, setComplianceResults] = useState([]); const handleRunTest = async () => { if (!accessToken) return; @@ -596,6 +642,43 @@ const PipelineTestPanel: React.FC = ({ } }; + const handleRunComplianceDataset = async () => { + if (!accessToken) return; + + const emptySteps = pipeline.steps.filter((s) => !s.guardrail); + if (emptySteps.length > 0) { + setError("All steps must have a guardrail selected"); + return; + } + + setError(null); + setResult(null); + setComplianceRunning(true); + const prompts = getComplianceDatasetPrompts(); + const entries: ComplianceRunEntry[] = []; + + for (const prompt of prompts) { + try { + const data = await testPipelineCall(accessToken, pipeline, [ + { role: "user", content: prompt.prompt }, + ]); + const matched = complianceMatchExpected(prompt.expectedResult, data.terminal_action); + entries.push({ prompt, result: data, matched }); + } catch (e) { + const errMsg = e instanceof Error ? e.message : String(e); + entries.push({ + prompt, + result: null, + error: errMsg, + matched: false, + }); + } + } + + setComplianceResults(entries); + setComplianceRunning(false); + }; + return (
= ({ +
{/* Results section */} @@ -773,11 +866,318 @@ const PipelineTestPanel: React.FC = ({ )} - {!result && !error && ( -
- Enter a test message and click "Run Test" to execute the pipeline + {complianceResults.length > 0 && ( +
+
+ Compliance dataset +
+
+ {complianceResults.filter((e) => e.matched).length} / {complianceResults.length} matched + expected +
+
+ {complianceResults.map((entry, i) => { + const actual = + entry.result?.terminal_action ?? (entry.error ? "error" : "—"); + const matchStyle = entry.matched + ? { bg: "#f0fdf4", color: "#16a34a" } + : { bg: "#fef2f2", color: "#dc2626" }; + return ( +
+
+ {entry.prompt.prompt} +
+
+ + expected: {entry.prompt.expectedResult} + + → + + actual: {actual} + + + {entry.matched ? "✓" : "✗"} + +
+ {entry.error && ( +
+ {entry.error} +
+ )} +
+ ); + })} +
)} + + {!result && !error && complianceResults.length === 0 && ( +
+ Enter a test message and click "Run Test" or "Test pipeline (compliance dataset)" to + execute the pipeline +
+ )} +
+ + ); +}; + +// ───────────────────────────────────────────────────────────────────────────── +// Policy Versions Sidebar (left sidebar when editing a policy) +// ───────────────────────────────────────────────────────────────────────────── + +const VERSION_STATUS_STYLES: Record< + string, + { bg: string; color: string } +> = { + draft: { bg: "#f3f4f6", color: "#6b7280" }, + published: { bg: "#eff6ff", color: "#2563eb" }, + production: { bg: "#f0fdf4", color: "#16a34a" }, +}; + +interface PolicyVersionsSidebarProps { + policyName: string; + editingPolicyId: string | null; + editingVersionStatus?: "draft" | "published" | "production"; + accessToken: string | null; + versions: Policy[]; + isLoading: boolean; + isCreatingVersion?: boolean; + isUpdatingStatus?: boolean; + onNewVersion: () => void; + onSelectVersion: (policy: Policy) => void; + onPublish?: () => void; + onPromoteToProduction?: () => void; +} + +const PolicyVersionsSidebar: React.FC = ({ + policyName, + editingPolicyId, + editingVersionStatus, + accessToken, + versions, + isLoading, + isCreatingVersion = false, + isUpdatingStatus = false, + onNewVersion, + onSelectVersion, + onPublish, + onPromoteToProduction, +}) => { + const canPublish = editingVersionStatus === "draft" && onPublish; + const canPromote = editingVersionStatus === "published" && onPromoteToProduction; + + return ( +
+
+ {/* Versions section */} +
+ + Versions + + + {isLoading ? ( +
+ +
+ ) : versions.length === 0 ? ( + + No versions found + + ) : ( +
+ {versions.map((v) => { + const statusStyle = + VERSION_STATUS_STYLES[v.version_status ?? "draft"] ?? + VERSION_STATUS_STYLES.draft; + const isActive = v.policy_id === editingPolicyId; + return ( + + ); + })} +
+ )} + + {/* Publish / Promote to production for selected version */} + {(canPublish || canPromote) && ( +
+ {canPublish && ( + + )} + {canPromote && ( + + )} +
+ )} +
+ + {/* Silent Mirroring section */} +
+
+ + Silent Mirroring + + + COMING SOON + +
+ + Test policy versions on production traffic without blocking requests. + Shadow testing helps validate changes before full rollout. + +
); @@ -795,6 +1195,9 @@ interface FlowBuilderPageProps { availableGuardrails: Guardrail[]; createPolicy: (accessToken: string, policyData: any) => Promise; updatePolicy: (accessToken: string, policyId: string, policyData: any) => Promise; + onVersionCreated?: (newPolicy: Policy) => void; + onSelectVersion?: (policy: Policy) => void; + onVersionStatusUpdated?: (updatedPolicy: Policy) => void; } export const FlowBuilderPage: React.FC = ({ @@ -805,16 +1208,110 @@ export const FlowBuilderPage: React.FC = ({ availableGuardrails, createPolicy, updatePolicy, + onVersionCreated, + onSelectVersion, + onVersionStatusUpdated, }) => { const isEditing = !!editingPolicy?.policy_id; + const showVersionsSidebar = !!editingPolicy?.policy_name; const [policyName, setPolicyName] = useState(editingPolicy?.policy_name || ""); const [description, setDescription] = useState(editingPolicy?.description || ""); const [isSubmitting, setIsSubmitting] = useState(false); const [showTestPanel, setShowTestPanel] = useState(false); const [pipeline, setPipeline] = useState( - editingPolicy?.pipeline || { mode: "pre_call", steps: [createDefaultStep()] } + () => derivePipelineFromPolicy(editingPolicy) ); + const [versions, setVersions] = useState([]); + const [isVersionsLoading, setIsVersionsLoading] = useState(false); + const [isCreatingVersion, setIsCreatingVersion] = useState(false); + const [isUpdatingStatus, setIsUpdatingStatus] = useState(false); + + // Sync local state when editingPolicy changes (e.g. user switched version) + React.useEffect(() => { + setPolicyName(editingPolicy?.policy_name || ""); + setDescription(editingPolicy?.description || ""); + setPipeline(derivePipelineFromPolicy(editingPolicy)); + }, [editingPolicy?.policy_id, editingPolicy?.policy_name, editingPolicy?.description, editingPolicy?.pipeline, editingPolicy?.guardrails_add]); + + // Fetch versions when editing an existing policy by name + React.useEffect(() => { + if (!showVersionsSidebar || !editingPolicy?.policy_name || !accessToken) { + setVersions([]); + return; + } + let cancelled = false; + setIsVersionsLoading(true); + listPolicyVersions(accessToken, editingPolicy.policy_name) + .then((res) => { + if (!cancelled) setVersions(res.versions || []); + }) + .catch(() => { + if (!cancelled) setVersions([]); + }) + .finally(() => { + if (!cancelled) setIsVersionsLoading(false); + }); + return () => { + cancelled = true; + }; + }, [showVersionsSidebar, editingPolicy?.policy_name, accessToken]); + + const handleNewVersion = async () => { + if (!accessToken || !editingPolicy?.policy_name) return; + setIsCreatingVersion(true); + try { + const newPolicy = await createPolicyVersion(accessToken, editingPolicy.policy_name); + NotificationsManager.success("New draft version created"); + onVersionCreated?.(newPolicy); + } catch (error) { + NotificationsManager.fromBackend( + "Failed to create version: " + (error instanceof Error ? error.message : String(error)) + ); + } finally { + setIsCreatingVersion(false); + } + }; + + const handleSelectVersion = (policy: Policy) => { + onSelectVersion?.(policy); + }; + + const handlePublishVersion = async () => { + if (!accessToken || !editingPolicy?.policy_id) return; + setIsUpdatingStatus(true); + try { + const updated = await updatePolicyVersionStatus(accessToken, editingPolicy.policy_id, "published"); + NotificationsManager.success("Version published"); + const list = await listPolicyVersions(accessToken, editingPolicy.policy_name ?? ""); + setVersions(list.versions ?? []); + onVersionStatusUpdated?.(updated); + } catch (error) { + NotificationsManager.fromBackend( + "Failed to publish: " + (error instanceof Error ? error.message : String(error)) + ); + } finally { + setIsUpdatingStatus(false); + } + }; + + const handlePromoteToProduction = async () => { + if (!accessToken || !editingPolicy?.policy_id) return; + setIsUpdatingStatus(true); + try { + const updated = await updatePolicyVersionStatus(accessToken, editingPolicy.policy_id, "production"); + NotificationsManager.success("Version promoted to production"); + const list = await listPolicyVersions(accessToken, editingPolicy.policy_name ?? ""); + setVersions(list.versions ?? []); + onVersionStatusUpdated?.(updated); + } catch (error) { + NotificationsManager.fromBackend( + "Failed to promote to production: " + (error instanceof Error ? error.message : String(error)) + ); + } finally { + setIsUpdatingStatus(false); + } + }; const handleSave = async () => { if (!policyName.trim()) { @@ -849,13 +1346,13 @@ export const FlowBuilderPage: React.FC = ({ if (isEditing && editingPolicy) { await updatePolicy(accessToken, editingPolicy.policy_id, data as PolicyUpdateRequest); NotificationsManager.success("Policy updated successfully"); + onSuccess(); } else { await createPolicy(accessToken, data as PolicyCreateRequest); NotificationsManager.success("Policy created successfully"); + onSuccess(); + onBack(); } - - onSuccess(); - onBack(); } catch (error) { console.error("Failed to save policy:", error); NotificationsManager.fromBackend( @@ -963,8 +1460,24 @@ export const FlowBuilderPage: React.FC = ({ /> - {/* Flow builder canvas + test panel */} + {/* Sidebar (when editing) + Flow builder canvas + test panel */}
+ {showVersionsSidebar && ( + + )}
(); + for (const p of policies) { + const name = p.policy_name || "(unnamed)"; + if (!byName.has(name)) byName.set(name, []); + byName.get(name)!.push(p); + } + const rows: PolicyRow[] = []; + for (const [policyName, versions] of byName) { + // Prefer production, then highest version_number + const primary = + versions.find((v) => v.version_status === "production") ?? + [...versions].sort((a, b) => (b.version_number ?? 0) - (a.version_number ?? 0))[0] ?? + versions[0]; + rows.push({ policy_name: policyName, primaryPolicy: primary, versionCount: versions.length }); + } + return rows.sort((a, b) => a.policy_name.localeCompare(b.policy_name)); +} + interface PolicyTableProps { policies: Policy[]; isLoading: boolean; @@ -29,49 +55,48 @@ const PolicyTable: React.FC = ({ onViewClick, isAdmin = false, }) => { - const [sorting, setSorting] = useState([{ id: "created_at", desc: true }]); + const [sorting, setSorting] = useState([{ id: "policy_name", desc: false }]); + + const rows = useMemo(() => groupPoliciesByName(policies), [policies]); - // Format date helper function const formatDate = (dateString?: string) => { if (!dateString) return "-"; const date = new Date(dateString); return date.toLocaleString(); }; - const columns: ColumnDef[] = [ - { - header: "Policy ID", - accessorKey: "policy_id", - cell: (info: any) => ( - - - - ), - }, + const columns: ColumnDef[] = [ { header: "Name", accessorKey: "policy_name", cell: ({ row }) => { - const policy = row.original; + const { primaryPolicy, versionCount } = row.original; return ( - - {policy.policy_name || "-"} - +
+ 1 ? ` (${versionCount} versions)` : ""}`}> + + + {versionCount > 1 && ( + + {versionCount} version{versionCount !== 1 ? "s" : ""} + + )} +
); }, }, { header: "Description", - accessorKey: "description", + accessorFn: (row) => row.primaryPolicy.description ?? "", cell: ({ row }) => { - const policy = row.original; + const policy = row.original.primaryPolicy; return ( @@ -83,9 +108,9 @@ const PolicyTable: React.FC = ({ }, { header: "Inherits From", - accessorKey: "inherit", + accessorFn: (row) => row.primaryPolicy.inherit ?? "", cell: ({ row }) => { - const policy = row.original; + const policy = row.original.primaryPolicy; return policy.inherit ? ( {policy.inherit} @@ -97,9 +122,9 @@ const PolicyTable: React.FC = ({ }, { header: "Guardrails (Add)", - accessorKey: "guardrails_add", + accessorFn: (row) => (row.primaryPolicy.guardrails_add ?? []).join(", "), cell: ({ row }) => { - const policy = row.original; + const policy = row.original.primaryPolicy; const guardrails = policy.guardrails_add || []; if (guardrails.length === 0) { return -; @@ -122,9 +147,9 @@ const PolicyTable: React.FC = ({ }, { header: "Guardrails (Remove)", - accessorKey: "guardrails_remove", + accessorFn: (row) => (row.primaryPolicy.guardrails_remove ?? []).join(", "), cell: ({ row }) => { - const policy = row.original; + const policy = row.original.primaryPolicy; const guardrails = policy.guardrails_remove || []; if (guardrails.length === 0) { return -; @@ -147,9 +172,12 @@ const PolicyTable: React.FC = ({ }, { header: "Model Condition", - accessorKey: "condition", + accessorFn: (row) => { + const m = row.primaryPolicy.condition?.model; + return typeof m === "string" ? m : JSON.stringify(m ?? ""); + }, cell: ({ row }) => { - const policy = row.original; + const policy = row.original.primaryPolicy; const modelCondition = policy.condition?.model; if (!modelCondition) { return -; @@ -169,9 +197,10 @@ const PolicyTable: React.FC = ({ }, { header: "Created At", - accessorKey: "created_at", + id: "created_at", + accessorFn: (row) => row.primaryPolicy.created_at ?? "", cell: ({ row }) => { - const policy = row.original; + const policy = row.original.primaryPolicy; return ( {formatDate(policy.created_at)} @@ -183,7 +212,8 @@ const PolicyTable: React.FC = ({ id: "actions", header: "Actions", cell: ({ row }) => { - const policy = row.original; + const { primaryPolicy } = row.original; + const policy = primaryPolicy; return (
{isAdmin && ( @@ -216,7 +246,7 @@ const PolicyTable: React.FC = ({ ]; const table = useReactTable({ - data: policies, + data: rows, columns, state: { sorting, @@ -273,9 +303,9 @@ const PolicyTable: React.FC = ({
- ) : policies.length > 0 ? ( + ) : rows.length > 0 ? ( table.getRowModel().rows.map((row) => ( - + {row.getVisibleCells().map((cell) => ( = { }, }; +/** Flat list of all compliance prompts for pipeline testing (EU AI Act, GDPR, topic blocking, airline, etc.). */ +export function getComplianceDatasetPrompts(): CompliancePrompt[] { + return getFrameworks().flatMap((fw) => + fw.categories.flatMap((cat) => cat.prompts) + ); +} + export function getFrameworks(): ComplianceFramework[] { const frameworkMap = new Map< string, diff --git a/ui/litellm-dashboard/tsconfig.json b/ui/litellm-dashboard/tsconfig.json index d24bdd340f7..5b0352feb98 100644 --- a/ui/litellm-dashboard/tsconfig.json +++ b/ui/litellm-dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "plugins": [ {