diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index e48c0fe3027..08f756e4fb0 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken { config Json @default("{}") user_id String? team_id String? + agent_id String? project_id String? permissions Json @default("{}") max_parallel_requests Int? diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index e48c0fe3027..08f756e4fb0 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken { config Json @default("{}") user_id String? team_id String? + agent_id String? project_id String? permissions Json @default("{}") max_parallel_requests Int? diff --git a/schema.prisma b/schema.prisma index 440c9c1d829..61de0073e0b 100644 --- a/schema.prisma +++ b/schema.prisma @@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken { config Json @default("{}") user_id String? team_id String? + agent_id String? project_id String? permissions Json @default("{}") max_parallel_requests Int? diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 258c2ccb0e0..df360d36dd8 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -38,7 +38,7 @@ import Usage from "@/components/usage"; import UserDashboard from "@/components/user_dashboard"; import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage"; import VectorStoreManagement from "@/components/vector_store_management"; -import ToolPolicies from "@/components/ToolPolicies"; +import ToolPoliciesView from "@/components/ToolPoliciesView"; import SpendLogsTable from "@/components/view_logs"; import ViewUserDashboard from "@/components/view_users"; import { ThemeProvider } from "@/contexts/ThemeContext"; @@ -550,7 +550,7 @@ function CreateKeyPageContent() { ) : page == "vector-stores" ? ( ) : page == "tool-policies" ? ( - + ) : page == "guardrails-monitor" ? ( ) : page == "new_usage" ? ( diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx new file mode 100644 index 00000000000..fa83190d7e3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx @@ -0,0 +1,317 @@ +"use client"; + +import { ArrowLeftOutlined, ToolOutlined } from "@ant-design/icons"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Button, Select, Spin } from "antd"; +import React, { useCallback, useMemo, useState } from "react"; +import TeamDropdown from "@/components/common_components/team_dropdown"; +import { PolicySelect } from "@/components/ToolPolicies/PolicySelect"; +import { + deleteToolPolicyOverride, + fetchToolDetail, + keyListCall, + teamListCall, + updateToolPolicy, + type ToolPolicyOverrideRow, +} from "@/components/networking"; +import type { Team } from "@/components/key_team_helpers/key_list"; + +interface ToolDetailProps { + toolName: string; + onBack: () => void; + accessToken: string | null; +} + +interface TeamOption { + team_id: string; + team_alias?: string; +} + +interface KeyOption { + token: string; + key_alias?: string; +} + +const TOOL_DETAIL_QUERY_KEY = "tool-detail"; + +export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { + const queryClient = useQueryClient(); + const [overrideSaving, setOverrideSaving] = useState(false); + const [policySaving, setPolicySaving] = useState(false); + const [blockScope, setBlockScope] = useState<"team" | "key">("team"); + const [blockTeamId, setBlockTeamId] = useState(null); + const [blockKey, setBlockKey] = useState(null); + + const { data: detail, isLoading: detailLoading, error: detailError } = useQuery({ + queryKey: [TOOL_DETAIL_QUERY_KEY, toolName], + queryFn: () => fetchToolDetail(accessToken!, toolName), + enabled: !!accessToken && !!toolName, + }); + + const { data: teamsData } = useQuery({ + queryKey: ["teams-list-tool-detail"], + queryFn: () => teamListCall(accessToken!, null, null), + enabled: !!accessToken, + }); + + const { data: keysData } = useQuery({ + queryKey: ["keys-list-tool-detail"], + queryFn: () => keyListCall(accessToken!, null, null, null, null, null, 1, 100), + enabled: !!accessToken, + }); + + const teams: Team[] = useMemo(() => { + const arr = Array.isArray(teamsData) ? teamsData : teamsData?.data ?? []; + return arr.map((t: { team_id?: string; id?: string; team_alias?: string }) => ({ + team_id: t.team_id ?? t.id ?? "", + team_alias: t.team_alias ?? t.team_id ?? "", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "", + created_at: "", + keys: [], + members_with_roles: [], + spend: 0, + })); + }, [teamsData]); + + const keys: KeyOption[] = useMemo(() => { + const keysRes = keysData?.keys ?? keysData?.data ?? []; + return keysRes.map((k: { token?: string; api_key?: string; key_hash?: string; key_alias?: string }) => ({ + token: k.token ?? k.api_key ?? k.key_hash ?? "", + key_alias: k.key_alias ?? (k.token ?? k.api_key ?? k.key_hash)?.toString?.()?.substring?.(0, 8), + })); + }, [keysData]); + + const invalidateDetail = useCallback(() => { + queryClient.invalidateQueries({ queryKey: [TOOL_DETAIL_QUERY_KEY, toolName] }); + }, [queryClient, toolName]); + + const handlePolicyChange = useCallback( + async (name: string, newPolicy: string) => { + if (!accessToken) return; + setPolicySaving(true); + try { + await updateToolPolicy(accessToken, name, newPolicy); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to update policy: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setPolicySaving(false); + } + }, + [accessToken, invalidateDetail] + ); + + const handleAddOverride = useCallback(async () => { + if (!accessToken || !toolName) return; + const isTeam = blockScope === "team"; + if (isTeam && !blockTeamId) return; + if (!isTeam && !blockKey?.token) return; + setOverrideSaving(true); + try { + await updateToolPolicy(accessToken, toolName, "blocked", { + team_id: isTeam ? blockTeamId : undefined, + key_hash: !isTeam ? blockKey!.token : undefined, + key_alias: !isTeam ? blockKey!.key_alias : undefined, + }); + invalidateDetail(); + setBlockTeamId(null); + setBlockKey(null); + } catch (e: unknown) { + alert(`Failed to add override: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOverrideSaving(false); + } + }, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail]); + + const handleRemoveOverride = useCallback( + async (override: ToolPolicyOverrideRow) => { + if (!accessToken || !toolName) return; + setOverrideSaving(true); + try { + await deleteToolPolicyOverride(accessToken, toolName, { + team_id: override.team_id ?? undefined, + key_hash: override.key_hash ?? undefined, + }); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to remove override: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOverrideSaving(false); + } + }, + [accessToken, toolName, invalidateDetail] + ); + + if (detailLoading && !detail) { + return ( + + + + ); + } + + if (detailError && !detail) { + return ( + + } onClick={onBack} className="pl-0 mb-4"> + Back to Tool Policies + + Failed to load tool details. + + ); + } + + if (!detail) { + return null; + } + + const { tool, overrides } = detail; + + return ( + + + } + onClick={onBack} + className="pl-0 mb-4" + > + Back to Tool Policies + + + + + + + {tool.tool_name} + + {tool.origin ?? "—"} + + + {(tool.call_count ?? 0).toLocaleString()} calls + + + + + + + + + Global policy + + + + {overrides.length > 0 && ( + + Blocked for team or key + + {overrides.map((ov) => ( + + + {ov.team_id ? `Team: ${ov.team_id}` : ""} + {ov.team_id && ov.key_hash ? " · " : ""} + {ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""} + {!ov.team_id && !ov.key_hash ? "—" : ""} + + handleRemoveOverride(ov)} + > + Remove + + + ))} + + + )} + + + Block for team or key + + + Scope + + + setBlockScope("team")} + className="align-middle" + /> + Team + + + setBlockScope("key")} + className="align-middle" + /> + Key + + + + + + {blockScope === "team" ? "Team" : "Key"} + + {blockScope === "team" ? ( + setBlockTeamId(id || null)} + /> + ) : ( + { + const k = keys.find((x) => x.token === token); + setBlockKey(k ?? null); + }} + options={keys.map((k) => ({ + value: k.token, + label: k.key_alias || k.token?.substring?.(0, 12) || k.token, + }))} + className="w-full" + style={{ minWidth: 200 }} + /> + )} + + + Block for {blockScope} + + + + + + ); +} diff --git a/ui/litellm-dashboard/src/components/ToolPolicies.tsx b/ui/litellm-dashboard/src/components/ToolPolicies.tsx index f39b21aed76..b90bd80e600 100644 --- a/ui/litellm-dashboard/src/components/ToolPolicies.tsx +++ b/ui/litellm-dashboard/src/components/ToolPolicies.tsx @@ -1,24 +1,19 @@ "use client"; import React, { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react"; -import { Button, Modal, Select, Switch, Tooltip } from "antd"; +import { Button, Switch, Tooltip } from "antd"; import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } from "@tremor/react"; import { TimeCell } from "./view_logs/time_cell"; import { TableHeaderSortDropdown } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown"; import type { SortState } from "./common_components/TableHeaderSortDropdown/TableHeaderSortDropdown"; import FilterComponent, { FilterOption } from "./molecules/filter"; import { MetricCard } from "./GuardrailsMonitor/MetricCard"; -import TeamDropdown from "./common_components/team_dropdown"; +import { PolicySelect, POLICY_OPTIONS } from "./ToolPolicies/PolicySelect"; import { - fetchToolDetail, fetchToolsList, updateToolPolicy, - deleteToolPolicyOverride, ToolRow, - ToolDetailResponse, - ToolPolicyOverrideRow, } from "./networking"; -import { teamListCall, keyListCall } from "./networking"; // --- Date helpers (UTC) for "new tools" counts --- function getUTCDateKey(date: Date): string { @@ -46,15 +41,6 @@ function getTrendSubtitle(newToday: number, newYesterday: number): string | unde return `${diff} since yesterday`; } -const POLICY_OPTIONS = [ - { value: "trusted", label: "trusted", color: "#065f46", bg: "#d1fae5", border: "#6ee7b7" }, - { value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" }, -] as const; - -type PolicyValue = "trusted" | "blocked"; - -const policyStyle = (p: string) => POLICY_OPTIONS.find((o) => o.value === p) ?? POLICY_OPTIONS[1]; - type SortField = "tool_name" | "call_policy" | "team_id" | "key_alias" | "created_at" | "call_count"; interface FilterValues { @@ -64,99 +50,16 @@ interface FilterValues { interface ToolPoliciesProps { accessToken: string | null; userRole?: string; + onSelectTool?: (toolName: string) => void; } -const PolicySelect: React.FC<{ - value: string; - toolName: string; - saving: boolean; - onChange: (toolName: string, policy: string) => void; -}> = ({ value, toolName, saving, onChange }) => { - const style = policyStyle(value); - return ( - onChange(toolName, v)} - onClick={(e) => e.stopPropagation()} - style={{ - minWidth: 110, - fontWeight: 500, - }} - styles={{ - selector: { - backgroundColor: style.bg, - borderColor: style.border, - color: style.color, - borderRadius: 999, - fontSize: 11, - fontWeight: 600, - paddingLeft: 8, - paddingRight: 4, - }, - }} - popupMatchSelectWidth={false} - options={POLICY_OPTIONS.map((o) => ({ - value: o.value, - label: ( - - - {o.label} - - ), - }))} - /> - ); -}; - -interface TeamOption { - team_id: string; - team_alias?: string; -} - -interface KeyOption { - token: string; - key_alias?: string; -} - -export const ToolPolicies: React.FC = ({ accessToken }) => { +export const ToolPolicies: React.FC = ({ accessToken, onSelectTool }) => { const [tools, setTools] = useState([]); const [loading, setLoading] = useState(true); const [isFetching, setIsFetching] = useState(false); const [error, setError] = useState(null); const [saving, setSaving] = useState(null); - const [detailModalOpen, setDetailModalOpen] = useState(false); - const [detailToolName, setDetailToolName] = useState(null); - const [detail, setDetail] = useState(null); - const [detailLoading, setDetailLoading] = useState(false); - const [teams, setTeams] = useState([]); - const [keys, setKeys] = useState([]); - const [overrideSaving, setOverrideSaving] = useState(false); - const [blockScope, setBlockScope] = useState<"team" | "key">("team"); - const [blockTeamId, setBlockTeamId] = useState(null); - const [blockKey, setBlockKey] = useState<{ token: string; key_alias?: string } | null>(null); - const [searchTerm, setSearchTerm] = useState(""); const [sortField, setSortField] = useState("created_at"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); @@ -199,9 +102,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { try { await updateToolPolicy(accessToken, toolName, newPolicy); setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, call_policy: newPolicy } : t))); - if (detailToolName === toolName && detail) { - setDetail((d) => (d ? { ...d, tool: { ...d.tool, call_policy: newPolicy } } : null)); - } } catch (e: any) { alert(`Failed to update policy: ${e.message}`); } finally { @@ -209,91 +109,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { } }; - const openDetailModal = useCallback( - async (toolName: string) => { - if (!accessToken) return; - setDetailToolName(toolName); - setDetailModalOpen(true); - setDetail(null); - setDetailLoading(true); - setBlockTeamId(null); - setBlockKey(null); - try { - const [detailRes, teamsRes, keysRes] = await Promise.all([ - fetchToolDetail(accessToken, toolName), - teamListCall(accessToken, null, null), - keyListCall(accessToken, null, null, null, null, null, 1, 100), - ]); - setDetail(detailRes); - const teamsArray = Array.isArray(teamsRes) ? teamsRes : teamsRes?.data ?? []; - setTeams( - teamsArray.map((t: any) => ({ team_id: t.team_id ?? t.id, team_alias: t.team_alias ?? t.team_id })) - ); - const keysArray = keysRes?.keys ?? keysRes?.data ?? []; - setKeys( - keysArray.map((k: any) => ({ - token: k.token ?? k.api_key ?? k.key_hash ?? "", - key_alias: k.key_alias ?? k.token?.substring?.(0, 8), - })) - ); - } catch (e: any) { - setError(e.message ?? "Failed to load tool detail"); - } finally { - setDetailLoading(false); - } - }, - [accessToken] - ); - - const closeDetailModal = useCallback(() => { - setDetailModalOpen(false); - setDetailToolName(null); - setDetail(null); - }, []); - - const handleAddOverride = useCallback(async () => { - if (!accessToken || !detailToolName) return; - const isTeam = blockScope === "team"; - if (isTeam && !blockTeamId) return; - if (!isTeam && !blockKey?.token) return; - setOverrideSaving(true); - try { - await updateToolPolicy(accessToken, detailToolName, "blocked", { - team_id: isTeam ? blockTeamId! : undefined, - key_hash: !isTeam ? blockKey!.token : undefined, - key_alias: !isTeam ? blockKey!.key_alias : undefined, - }); - const refreshed = await fetchToolDetail(accessToken, detailToolName); - setDetail(refreshed); - setBlockTeamId(null); - setBlockKey(null); - } catch (e: any) { - alert(`Failed to add override: ${e.message}`); - } finally { - setOverrideSaving(false); - } - }, [accessToken, detailToolName, blockScope, blockTeamId, blockKey]); - - const handleRemoveOverride = useCallback( - async (override: ToolPolicyOverrideRow) => { - if (!accessToken || !detailToolName) return; - setOverrideSaving(true); - try { - await deleteToolPolicyOverride(accessToken, detailToolName, { - team_id: override.team_id ?? undefined, - key_hash: override.key_hash ?? undefined, - }); - const refreshed = await fetchToolDetail(accessToken, detailToolName); - setDetail(refreshed); - } catch (e: any) { - alert(`Failed to remove override: ${e.message}`); - } finally { - setOverrideSaving(false); - } - }, - [accessToken, detailToolName] - ); - const handleSortChange = (field: SortField, newState: SortState) => { if (newState === false) { setSortField("created_at"); @@ -431,7 +246,7 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { }; return ( - + Tool Policies {/* Summary cards */} @@ -644,10 +459,10 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { openDetailModal(tool.tool_name)} + onClick={() => onSelectTool?.(tool.tool_name)} className="text-left w-full font-mono text-xs max-w-[20ch] truncate block font-medium text-blue-600 hover:text-blue-800 hover:underline focus:outline-none focus:ring-0" > - + {tool.tool_name} @@ -660,8 +475,10 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { onChange={handlePolicyChange} /> - - {(tool.call_count ?? 0).toLocaleString()} + + + {(tool.call_count ?? 0).toLocaleString()} + @@ -718,134 +535,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { )} - {/* Tool detail modal: view tool, global policy, overrides, block for team/key */} - - {detailLoading ? ( - Loading… - ) : detail ? ( - - - - Origin: {detail.tool.origin ?? "—"} - - - # Calls: {(detail.tool.call_count ?? 0).toLocaleString()} - - - - Global policy - - - - {detail.overrides.length > 0 && ( - - Blocked for team/key - - {detail.overrides.map((ov) => ( - - - {ov.team_id ? `Team: ${ov.team_id}` : ""} - {ov.team_id && ov.key_hash ? " · " : ""} - {ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""} - {!ov.team_id && !ov.key_hash ? "—" : ""} - - handleRemoveOverride(ov)} - > - Remove - - - ))} - - - )} - - - Block for team or key - - - - setBlockScope("team")} - className="mr-1" - /> - Team - - - setBlockScope("key")} - className="mr-1" - /> - Key - - - {blockScope === "team" ? ( - - setBlockTeamId(id ?? null)} - /> - - ) : ( - { - const k = keys.find((x) => x.token === token); - setBlockKey(k ?? null); - }} - options={keys.map((k) => ({ - value: k.token, - label: k.key_alias || k.token?.substring?.(0, 12) || k.token, - }))} - style={{ minWidth: 200 }} - /> - )} - - Block for {blockScope} - - - - - ) : ( - No data - )} - ); }; diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx new file mode 100644 index 00000000000..c8cc5480e46 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx @@ -0,0 +1,90 @@ +"use client"; + +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" }, + { value: "untrusted", label: "untrusted", color: "#92400e", bg: "#fef3c7", border: "#fcd34d" }, + { value: "blocked", label: "blocked", color: "#991b1b", bg: "#fee2e2", border: "#fca5a5" }, +] as const; + +export const policyStyle = (p: string) => + POLICY_OPTIONS.find((o) => o.value === p) ?? POLICY_OPTIONS[1]; + +export interface PolicySelectProps { + value: string; + toolName: string; + saving: boolean; + onChange: (toolName: string, policy: string) => void; + size?: "small" | "middle"; + minWidth?: number; + stopPropagation?: boolean; +} + +export const PolicySelect: React.FC = ({ + value, + toolName, + saving, + onChange, + size = "small", + minWidth = 110, + stopPropagation = true, +}) => { + const style = policyStyle(value); + return ( + onChange(toolName, v)} + onClick={(e) => stopPropagation && e.stopPropagation()} + style={{ + minWidth, + fontWeight: 500, + }} + styles={{ + selector: { + backgroundColor: style.bg, + borderColor: style.border, + color: style.color, + borderRadius: 999, + fontSize: size === "small" ? 11 : 12, + fontWeight: 600, + paddingLeft: 8, + paddingRight: 4, + }, + }} + popupMatchSelectWidth={false} + options={POLICY_OPTIONS.map((o) => ({ + value: o.value, + label: ( + + + {o.label} + + ), + }))} + /> + ); +}; diff --git a/ui/litellm-dashboard/src/components/ToolPoliciesView.tsx b/ui/litellm-dashboard/src/components/ToolPoliciesView.tsx new file mode 100644 index 00000000000..3e4964f7e36 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolPoliciesView.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React, { useState } from "react"; +import { ToolDetail } from "@/components/ToolDetail"; +import { ToolPolicies } from "@/components/ToolPolicies"; + +type View = + | { type: "overview" } + | { type: "detail"; toolName: string }; + +interface ToolPoliciesViewProps { + accessToken: string | null; + userRole?: string; +} + +export default function ToolPoliciesView({ accessToken, userRole }: ToolPoliciesViewProps) { + const [view, setView] = useState({ type: "overview" }); + + const handleSelectTool = (toolName: string) => { + setView({ type: "detail", toolName }); + }; + + const handleBack = () => { + setView({ type: "overview" }); + }; + + return ( + + {view.type === "detail" ? ( + + ) : ( + + )} + + ); +}
Failed to load tool details.
Loading…
No data