From f619287e84f537782abc4a5868af2fbd589553af Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 3 Mar 2026 19:49:10 -0800 Subject: [PATCH] input/output policy --- .../src/components/ToolPolicies.tsx | 96 ++++++++++++------- .../components/ToolPolicies/PolicySelect.tsx | 36 +++---- 2 files changed, 78 insertions(+), 54 deletions(-) diff --git a/ui/litellm-dashboard/src/components/ToolPolicies.tsx b/ui/litellm-dashboard/src/components/ToolPolicies.tsx index b90bd80e600..c18d9b97d7b 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies.tsx @@ -8,14 +8,13 @@ import { TableHeaderSortDropdown } from "./common_components/TableHeaderSortDrop import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown"; import FilterComponent, { FilterOption } from "./molecules/filter"; import { MetricCard } from "./GuardrailsMonitor/MetricCard"; -import { PolicySelect, POLICY_OPTIONS } from "./ToolPolicies/PolicySelect"; +import { PolicySelect, INPUT_POLICY_OPTIONS, OUTPUT_POLICY_OPTIONS } from "./ToolPolicies/PolicySelect"; import { fetchToolsList, updateToolPolicy, ToolRow, } from "./networking"; -// --- Date helpers (UTC) for "new tools" counts --- function getUTCDateKey(date: Date): string { return `${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`; } @@ -41,7 +40,7 @@ function getTrendSubtitle(newToday: number, newYesterday: number): string | unde return `${diff} since yesterday`; } -type SortField = "tool_name" | "call_policy" | "team_id" | "key_alias" | "created_at" | "call_count"; +type SortField = "tool_name" | "input_policy" | "output_policy" | "team_id" | "key_alias" | "created_at" | "call_count"; interface FilterValues { [key: string]: string; @@ -58,7 +57,8 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec const [loading, setLoading] = useState(true); const [isFetching, setIsFetching] = useState(false); const [error, setError] = useState(null); - const [saving, setSaving] = useState(null); + const [savingInput, setSavingInput] = useState(null); + const [savingOutput, setSavingOutput] = useState(null); const [searchTerm, setSearchTerm] = useState(""); const [sortField, setSortField] = useState("created_at"); @@ -96,16 +96,29 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec return () => clearInterval(id); }, [isLiveTail, load]); - const handlePolicyChange = async (toolName: string, newPolicy: string) => { + const handleInputPolicyChange = async (toolName: string, newPolicy: string) => { if (!accessToken) return; - setSaving(toolName); + setSavingInput(toolName); try { - await updateToolPolicy(accessToken, toolName, newPolicy); - setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, call_policy: newPolicy } : t))); + await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy }); + setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, input_policy: newPolicy } : t))); } catch (e: any) { - alert(`Failed to update policy: ${e.message}`); + alert(`Failed to update input policy: ${e.message}`); } finally { - setSaving(null); + setSavingInput(null); + } + }; + + const handleOutputPolicyChange = async (toolName: string, newPolicy: string) => { + if (!accessToken) return; + setSavingOutput(toolName); + try { + await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy }); + setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, output_policy: newPolicy } : t))); + } catch (e: any) { + alert(`Failed to update output policy: ${e.message}`); + } finally { + setSavingOutput(null); } }; @@ -130,7 +143,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec setCurrentPage(1); }; - // Build unique team/key options from loaded data const teamOptions = Array.from(new Set(tools.map((t) => t.team_id).filter(Boolean))).map((v) => ({ label: v as string, value: v as string, @@ -142,9 +154,14 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec const filterOptions: FilterOption[] = [ { - name: "Policy", - label: "Policy", - options: POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), + name: "Input Policy", + label: "Input Policy", + options: INPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), + }, + { + name: "Output Policy", + label: "Output Policy", + options: OUTPUT_POLICY_OPTIONS.map((o) => ({ label: o.label, value: o.value })), }, { name: "Team Name", @@ -158,7 +175,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec }, ]; - // Derived counts for summary cards and "Needs Review" (UTC today/yesterday) const { newToday, newYesterday, trendSubtitle, totalTools, blockedCount, activeTeamsCount, needsReviewTools } = useMemo(() => { const now = new Date(); @@ -172,14 +188,13 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec const trendSubtitle = getTrendSubtitle(newToday, newYesterday); const totalTools = tools.length; - const blockedCount = tools.filter((t) => t.call_policy === "blocked").length; + const blockedCount = tools.filter((t) => t.input_policy === "blocked").length; const activeTeamsCount = new Set(tools.map((t) => t.team_id).filter(Boolean)).size; - // New in period (today) and not yet decided — untrusted or dual_llm const needsReviewTools = tools.filter( (t) => isCreatedInUTCDay(t.created_at, todayKey) && - (t.call_policy === "untrusted" || t.call_policy === "dual_llm") + t.input_policy === "untrusted" ); return { @@ -211,10 +226,12 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec (t.team_id ?? "").toLowerCase().includes(q) || (t.key_alias ?? "").toLowerCase().includes(q) || (t.key_hash ?? "").toLowerCase().includes(q) || - t.call_policy.toLowerCase().includes(q); + t.input_policy.toLowerCase().includes(q) || + t.output_policy.toLowerCase().includes(q); if (!matchesSearch) return false; } - if (activeFilters["Policy"] && t.call_policy !== activeFilters["Policy"]) return false; + if (activeFilters["Input Policy"] && t.input_policy !== activeFilters["Input Policy"]) return false; + if (activeFilters["Output Policy"] && t.output_policy !== activeFilters["Output Policy"]) return false; if (activeFilters["Team Name"] && t.team_id !== activeFilters["Team Name"]) return false; if (activeFilters["Key Name"] && t.key_alias !== activeFilters["Key Name"]) return false; return true; @@ -236,7 +253,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec if (idx >= 0) { const page = Math.floor(idx / pageSize) + 1; if (page !== currentPage) setCurrentPage(page); - // Scroll after a short delay so the table has re-rendered with the new page requestAnimationFrame(() => { setTimeout(() => { document.getElementById(`tool-row-${toolId}`)?.scrollIntoView({ behavior: "smooth", block: "center" }); @@ -249,7 +265,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec

Tool Policies

- {/* Summary cards */}
= ({ accessToken, onSelec 0 ? activeTeamsCount : "—"} />
- {/* Needs Review */} {needsReviewTools.length > 0 && (

Needs Review

@@ -302,7 +316,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec )}
- {/* Toolbar */}
@@ -386,7 +399,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec
- {/* Filter row */}
= ({ accessToken, onSelec
- {/* Auto-refresh banner */} {isLiveTail && (
Auto-refreshing every 15 seconds @@ -411,7 +422,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec
{error}
)} - {/* Table */} @@ -422,7 +432,10 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec - + + + + @@ -434,19 +447,19 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec - Origin + User Agent {loading ? ( - + Loading tools… ) : paginated.length === 0 ? ( - + No tools discovered yet. Make a chat completion that returns tool_calls to start auto-discovery. @@ -469,10 +482,20 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec + + + @@ -498,8 +521,8 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec - - {tool.origin ?? "-"} + + {tool.user_agent ?? "-"} @@ -508,7 +531,6 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec
- {/* Bottom pagination (only when > 1 page) */} {totalPages > 1 && (
diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx index c8cc5480e46..1317351931e 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx @@ -3,21 +3,28 @@ import React from "react"; import { Select } from "antd"; -// DB policy values: "trusted" | "untrusted" | "dual_llm" | "blocked" — we expose all except dual_llm in the Policy dropdown -export const POLICY_OPTIONS = [ - { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, +export const INPUT_POLICY_OPTIONS = [ { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, + { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, { value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" }, ] as const; +export const OUTPUT_POLICY_OPTIONS = [ + { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, + { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, +] as const; + +export const POLICY_OPTIONS = INPUT_POLICY_OPTIONS; + export const policyStyle = (p: string) => - POLICY_OPTIONS.find((o) => o.value === p) ?? POLICY_OPTIONS[1]; + INPUT_POLICY_OPTIONS.find((o) => o.value === p) ?? INPUT_POLICY_OPTIONS[0]; export interface PolicySelectProps { value: string; toolName: string; saving: boolean; onChange: (toolName: string, policy: string) => void; + policyType?: "input" | "output"; size?: "small" | "middle"; minWidth?: number; stopPropagation?: boolean; @@ -28,10 +35,12 @@ export const PolicySelect: React.FC = ({ toolName, saving, onChange, + policyType = "input", size = "small", minWidth = 110, stopPropagation = true, }) => { + const options = policyType === "output" ? OUTPUT_POLICY_OPTIONS : INPUT_POLICY_OPTIONS; const style = policyStyle(value); return (