From 03ba307618c7cf95c809efcf4f2cbd654303412b Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Thu, 5 Feb 2026 14:42:29 -0800 Subject: [PATCH] add working demo files --- .../src/components/per_user_usage.tsx | 162 +++++-- .../src/components/user_agent_activity.tsx | 424 ++++++++++++++---- 2 files changed, 459 insertions(+), 127 deletions(-) diff --git a/ui/litellm-dashboard/src/components/per_user_usage.tsx b/ui/litellm-dashboard/src/components/per_user_usage.tsx index 59aed14292e..957bc9c9366 100644 --- a/ui/litellm-dashboard/src/components/per_user_usage.tsx +++ b/ui/litellm-dashboard/src/components/per_user_usage.tsx @@ -41,12 +41,116 @@ interface PerUserAnalyticsResponse { interface PerUserUsageProps { accessToken: string | null; selectedTags: string[]; + selectedTeam?: string; // DEMO: Team filter + selectedAgents?: string[]; // DEMO: Agent filter formatAbbreviatedNumber: (value: number, decimalPlaces?: number) => string; } -const PerUserUsage: React.FC = ({ accessToken, selectedTags, formatAbbreviatedNumber }) => { +// DEMO: Clean agent name mapping for display +const AGENT_DISPLAY_NAMES: Record = { + "claude-code": "Claude Code", + "claude-code-max": "Claude Code Max", + "codex-cli": "Codex CLI", + "GithubCopilot": "GitHub Copilot IDE", + "go-gh": "GitHub Copilot CLI", +}; + +// DEMO: Helper function to clean agent names for display +const cleanAgentName = (raw: string | null): string => { + if (!raw) return "Unknown"; + + // Strip "User-Agent:" prefix + let name = raw.replace(/^User-Agent:\s*/i, ""); + + // Handle versioned strings (e.g., "GithubCopilot/1.155.0") + if (name.startsWith("GithubCopilot")) return "GitHub Copilot IDE"; + if (name === "go-gh") return "GitHub Copilot CLI"; + if (name.startsWith("Mozilla/")) return "Browser"; + + return AGENT_DISPLAY_NAMES[name] || name; +}; + +// DEMO: Helper function to get auth method based on agent name +const getAuthMethod = (agentName: string | null): string => { + const clean = cleanAgentName(agentName); + + if (clean === "Claude Code Max") return "Client OAuth (Anthropic)"; + if (clean === "GitHub Copilot IDE") return "Client OAuth (GitHub)"; + if (clean === "GitHub Copilot CLI") return "Client OAuth (GitHub)"; + + // Claude Code and Codex CLI use proxy-managed keys + return "AI Gateway Key"; +}; + +// DEMO: Auth badge component with colored styling and clear distinction +const AuthBadge: React.FC<{ method: string }> = ({ method }) => { + const isClientOAuth = method.startsWith("Client OAuth"); + + // Client OAuth = green (user's own credentials, client-side) + // AI Gateway Key = blue (proxy-managed, server-side) + const style: React.CSSProperties = { + display: "inline-block", + padding: "3px 10px", + borderRadius: "6px", + fontSize: "12px", + fontWeight: 600, + backgroundColor: isClientOAuth ? "#dcfce7" : "#dbeafe", // green-100 or blue-100 + color: isClientOAuth ? "#166534" : "#1e40af", // green-800 or blue-800 + border: isClientOAuth ? "1px solid #86efac" : "1px solid #93c5fd", // green-300 or blue-300 + }; + + return {method}; +}; + +// DEMO: Extended interface for demo users with auth method and team +interface DemoUserMetrics extends PerUserMetrics { + auth_method: string; + team: string; +} + +// DEMO: User to team mapping +const USER_TEAM_MAP: Record = { + "alice@company.com": "Operations AI", + "bob@company.com": "Dispatch", + "charlie@company.com": "Platform", + "diana@company.com": "Voice", + "eve@company.com": "Platform", + "frank@company.com": "Dispatch", + "grace@company.com": "Voice", +}; + +// DEMO: Hardcoded per-user data for demo purposes with auth method and team +// - "AI Gateway Key" = credentials managed by LiteLLM proxy (server-side) +// - "Client OAuth (Provider)" = user's own OAuth credentials (client-side, BYOK) +const DEMO_USERS: DemoUserMetrics[] = [ + { user_id: "user-1", user_email: "alice@company.com", team: "Operations AI", auth_method: "AI Gateway Key", user_agent: "Claude Code", successful_requests: 2841, failed_requests: 8, total_requests: 2849, total_tokens: 3800000, spend: 176.20 }, + { user_id: "user-2", user_email: "bob@company.com", team: "Dispatch", auth_method: "AI Gateway Key", user_agent: "Codex CLI", successful_requests: 2103, failed_requests: 7, total_requests: 2110, total_tokens: 2900000, spend: 129.45 }, + { user_id: "user-3", user_email: "charlie@company.com", team: "Platform", auth_method: "Client OAuth (GitHub)", user_agent: "GitHub Copilot IDE", successful_requests: 5612, failed_requests: 3, total_requests: 5615, total_tokens: 6800000, spend: 203.12 }, + { user_id: "user-4", user_email: "diana@company.com", team: "Voice", auth_method: "Client OAuth (Anthropic)", user_agent: "Claude Code Max", successful_requests: 1987, failed_requests: 8, total_requests: 1995, total_tokens: 2400000, spend: 112.80 }, + { user_id: "user-5", user_email: "eve@company.com", team: "Platform", auth_method: "Client OAuth (GitHub)", user_agent: "GitHub Copilot IDE", successful_requests: 4231, failed_requests: 2, total_requests: 4233, total_tokens: 5100000, spend: 152.67 }, + { user_id: "user-6", user_email: "frank@company.com", team: "Dispatch", auth_method: "Client OAuth (GitHub)", user_agent: "GitHub Copilot CLI", successful_requests: 1876, failed_requests: 3, total_requests: 1879, total_tokens: 2600000, spend: 98.34 }, + { user_id: "user-7", user_email: "grace@company.com", team: "Voice", auth_method: "AI Gateway Key", user_agent: "Claude Code", successful_requests: 1542, failed_requests: 2, total_requests: 1544, total_tokens: 2100000, spend: 89.50 }, +]; + +const PerUserUsage: React.FC = ({ accessToken, selectedTags, selectedTeam, selectedAgents, formatAbbreviatedNumber }) => { // Maximum number of user agent categories to show in charts to prevent color palette overflow const MAX_USER_AGENTS = 8; + + // DEMO: Filter users by selected team and selected agents + const filteredUsers = DEMO_USERS.filter((user) => { + // Filter by team + if (selectedTeam && selectedTeam !== "All Teams" && user.team !== selectedTeam) { + return false; + } + // Filter by agents + if (selectedAgents && selectedAgents.length > 0) { + const userAgentClean = cleanAgentName(user.user_agent); + if (!selectedAgents.includes(userAgentClean)) { + return false; + } + } + return true; + }); const [perUserData, setPerUserData] = useState({ results: [], total_count: 0, @@ -105,13 +209,15 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, - {/* Tab 1: Existing User Details Table */} + {/* DEMO: Tab 1 - User Details Table with hardcoded demo data, Team, and Auth Method columns */} User ID User Email + Team + Auth Method User Agent Success Generations Total Tokens @@ -120,7 +226,8 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, - {perUserData.results.slice(0, 10).map((item: PerUserMetrics, index: number) => ( + {/* DEMO: Using filtered demo users data with team and auth method */} + {filteredUsers.map((item: DemoUserMetrics, index: number) => ( {item.user_id} @@ -129,7 +236,13 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, {item.user_email || "N/A"} - {item.user_agent || "Unknown"} + {item.team} + + + + + + {cleanAgentName(item.user_agent)} {formatAbbreviatedNumber(item.successful_requests)} @@ -141,34 +254,15 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, {formatAbbreviatedNumber(item.failed_requests)} - ${formatAbbreviatedNumber(item.spend, 4)} + ${formatAbbreviatedNumber(item.spend, 2)} ))}
- - {perUserData.results.length > 10 && ( -
- Showing 10 of {perUserData.total_count} results -
- - -
-
- )}
- {/* Tab 2: Usage Distribution Histogram */} + {/* DEMO: Tab 2 - Usage Distribution Histogram with filtered demo data */}
User Usage Distribution @@ -177,10 +271,10 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, { - // Get top user agents by frequency first + // DEMO: Get unique clean agent names from filtered demo data const userAgentCounts = new Map(); - perUserData.results.forEach((item: PerUserMetrics) => { - const agent = item.user_agent || "Unknown"; + filteredUsers.forEach((item: PerUserMetrics) => { + const agent = cleanAgentName(item.user_agent); userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1); }); @@ -199,10 +293,10 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, "100K+ requests": { range: [100000, Infinity], agents: {} as Record }, }; - // Count users in each category by user agent (only for top user agents) - perUserData.results.forEach((item: PerUserMetrics) => { + // DEMO: Count users in each category by user agent (using filtered data) + filteredUsers.forEach((item: PerUserMetrics) => { const successCount = item.successful_requests; - const userAgent = item.user_agent || "Unknown"; + const userAgent = cleanAgentName(item.user_agent); // Only process if this is one of the top user agents if (topUserAgents.includes(userAgent)) { @@ -231,10 +325,10 @@ const PerUserUsage: React.FC = ({ accessToken, selectedTags, })()} index="category" categories={(() => { - // Count user agents by frequency and get top ones + // DEMO: Count user agents by frequency and get top ones with clean names (using filtered data) const userAgentCounts = new Map(); - perUserData.results.forEach((item: PerUserMetrics) => { - const agent = item.user_agent || "Unknown"; + filteredUsers.forEach((item: PerUserMetrics) => { + const agent = cleanAgentName(item.user_agent); userAgentCounts.set(agent, (userAgentCounts.get(agent) || 0) + 1); }); diff --git a/ui/litellm-dashboard/src/components/user_agent_activity.tsx b/ui/litellm-dashboard/src/components/user_agent_activity.tsx index 8b5f303670d..708fc33c0d1 100644 --- a/ui/litellm-dashboard/src/components/user_agent_activity.tsx +++ b/ui/litellm-dashboard/src/components/user_agent_activity.tsx @@ -12,8 +12,14 @@ import { TabList, TabPanel, TabPanels, + Table, + TableHead, + TableRow, + TableHeaderCell, + TableBody, + TableCell, } from "@tremor/react"; -import { Select, Tooltip } from "antd"; +import { Select } from "antd"; import { userAgentSummaryCall, tagDauCall, tagWauCall, tagMauCall, tagDistinctCall } from "./networking"; import PerUserUsage from "./per_user_usage"; import { DateRangePickerValue } from "@tremor/react"; @@ -61,6 +67,200 @@ interface UserAgentActivityProps { onDateChange?: (value: DateRangePickerValue) => void; // Optional - not used anymore } +// DEMO: Clean agent name mapping for display +const AGENT_DISPLAY_NAMES: Record = { + "claude-code": "Claude Code", + "claude-code-max": "Claude Code Max", + "codex-cli": "Codex CLI", + "GithubCopilot": "GitHub Copilot IDE", + "go-gh": "GitHub Copilot CLI", +}; + +// DEMO: Helper function to clean agent names for display +const cleanAgentName = (raw: string): string => { + // Strip "User-Agent:" prefix + let name = raw.replace(/^User-Agent:\s*/i, ""); + + // Handle versioned strings (e.g., "GithubCopilot/1.155.0") + if (name.startsWith("GithubCopilot")) return "GitHub Copilot IDE"; + if (name === "go-gh") return "GitHub Copilot CLI"; + if (name.startsWith("Mozilla/")) return "Browser"; + + return AGENT_DISPLAY_NAMES[name] || name; +}; + +// DEMO: Hardcoded agent summary data for demo purposes +const DEMO_AGENTS = [ + { + name: "GitHub Copilot IDE", + raw_ua: "GithubCopilot/1.155.0", + success_requests: 34219, + total_tokens: 42100000, + total_cost: 1247.92, + }, + { + name: "Claude Code", + raw_ua: "claude-code", + success_requests: 12847, + total_tokens: 18200000, + total_cost: 842.30, + }, + { + name: "Claude Code Max", + raw_ua: "claude-code-max", + success_requests: 8421, + total_tokens: 11700000, + total_cost: 523.18, + }, + { + name: "Codex CLI", + raw_ua: "codex-cli", + success_requests: 5102, + total_tokens: 7100000, + total_cost: 312.45, + }, + { + name: "GitHub Copilot CLI", + raw_ua: "go-gh", + success_requests: 2340, + total_tokens: 3200000, + total_cost: 98.67, + }, +]; + +// DEMO: Hardcoded DAU data for demo purposes (7 days with realistic weekday/weekend pattern) +const DEMO_DAU = [ + { date: "2026-01-30", "GitHub Copilot IDE": 18, "Claude Code": 12, "Claude Code Max": 6, "Codex CLI": 4, "GitHub Copilot CLI": 3 }, + { date: "2026-01-31", "GitHub Copilot IDE": 20, "Claude Code": 14, "Claude Code Max": 7, "Codex CLI": 5, "GitHub Copilot CLI": 3 }, + { date: "2026-02-01", "GitHub Copilot IDE": 15, "Claude Code": 10, "Claude Code Max": 5, "Codex CLI": 3, "GitHub Copilot CLI": 2 }, + { date: "2026-02-02", "GitHub Copilot IDE": 14, "Claude Code": 9, "Claude Code Max": 4, "Codex CLI": 3, "GitHub Copilot CLI": 2 }, + { date: "2026-02-03", "GitHub Copilot IDE": 22, "Claude Code": 16, "Claude Code Max": 8, "Codex CLI": 6, "GitHub Copilot CLI": 4 }, + { date: "2026-02-04", "GitHub Copilot IDE": 24, "Claude Code": 18, "Claude Code Max": 9, "Codex CLI": 7, "GitHub Copilot CLI": 5 }, + { date: "2026-02-05", "GitHub Copilot IDE": 26, "Claude Code": 19, "Claude Code Max": 10, "Codex CLI": 8, "GitHub Copilot CLI": 5 }, +]; + +// DEMO: Agent names for chart categories (excludes Browser/Mozilla) +const DEMO_AGENT_CATEGORIES = ["GitHub Copilot IDE", "Claude Code", "Claude Code Max", "Codex CLI", "GitHub Copilot CLI"]; + +// DEMO: Available teams for filtering +const DEMO_TEAMS_LIST = ["All Teams", "Operations AI", "Platform", "Voice", "Dispatch"]; + +// DEMO: Budget data per agent (monthly budget and current usage) +const DEMO_BUDGETS: Record = { + "GitHub Copilot IDE": { budget: 2000, used: 1248 }, + "Claude Code": { budget: 1500, used: 842 }, + "Claude Code Max": { budget: 1000, used: 523 }, + "Codex CLI": { budget: 500, used: 312 }, + "GitHub Copilot CLI": { budget: 200, used: 99 }, +}; + +// DEMO: Guardrails enabled per agent +const DEMO_GUARDRAILS: Record = { + "GitHub Copilot IDE": ["PII Redaction", "IP Protection"], + "Claude Code": ["PII Redaction", "Prompt Injection"], + "Claude Code Max": ["PII Redaction", "Prompt Injection"], + "Codex CLI": ["PII Redaction"], + "GitHub Copilot CLI": ["PII Redaction"], +}; + +// DEMO: User data for computing team stats (mirrors per_user_usage.tsx) +const DEMO_USERS_FOR_TEAMS = [ + { user_email: "alice@company.com", team: "Operations AI", user_agent: "Claude Code", successful_requests: 2841, total_tokens: 3800000, spend: 176.20 }, + { user_email: "bob@company.com", team: "Dispatch", user_agent: "Codex CLI", successful_requests: 2103, total_tokens: 2900000, spend: 129.45 }, + { user_email: "charlie@company.com", team: "Platform", user_agent: "GitHub Copilot IDE", successful_requests: 5612, total_tokens: 6800000, spend: 203.12 }, + { user_email: "diana@company.com", team: "Voice", user_agent: "Claude Code Max", successful_requests: 1987, total_tokens: 2400000, spend: 112.80 }, + { user_email: "eve@company.com", team: "Platform", user_agent: "GitHub Copilot IDE", successful_requests: 4231, total_tokens: 5100000, spend: 152.67 }, + { user_email: "frank@company.com", team: "Dispatch", user_agent: "GitHub Copilot CLI", successful_requests: 1876, total_tokens: 2600000, spend: 98.34 }, + { user_email: "grace@company.com", team: "Voice", user_agent: "Claude Code", successful_requests: 1542, total_tokens: 2100000, spend: 89.50 }, +]; + +// DEMO: Function to compute team stats from filtered user data +const computeTeamStats = (users: typeof DEMO_USERS_FOR_TEAMS, selectedAgents: string[]) => { + // Filter users by selected agents + const filteredUsers = users.filter((user) => { + if (selectedAgents.length === 0) return true; + return selectedAgents.includes(user.user_agent); + }); + + // Group by team + const teamMap = new Map(); + + filteredUsers.forEach((user) => { + const existing = teamMap.get(user.team) || { active_users: 0, total_requests: 0, total_tokens: 0, total_cost: 0 }; + teamMap.set(user.team, { + active_users: existing.active_users + 1, + total_requests: existing.total_requests + user.successful_requests, + total_tokens: existing.total_tokens + user.total_tokens, + total_cost: existing.total_cost + user.spend, + }); + }); + + // Convert to array and sort by total_requests + return Array.from(teamMap.entries()) + .map(([team, stats]) => ({ team, ...stats })) + .sort((a, b) => b.total_requests - a.total_requests); +}; + +// DEMO: Budget progress bar component +const BudgetProgressBar: React.FC<{ budget: number; used: number }> = ({ budget, used }) => { + const percentage = Math.round((used / budget) * 100); + + // Color based on percentage: green < 60%, amber 60-80%, red > 80% + let fillColor = "#22c55e"; // green + if (percentage >= 80) { + fillColor = "#ef4444"; // red + } else if (percentage >= 60) { + fillColor = "#eab308"; // amber + } + + return ( +
+
+ Budget: ${budget.toLocaleString()}/mo + {percentage}% +
+
+
+
+
+ ); +}; + +// DEMO: Guardrails line component +const GuardrailsLine: React.FC<{ guardrails: string[] }> = ({ guardrails }) => { + if (!guardrails || guardrails.length === 0) return null; + + return ( +
+ + + {guardrails.join(", ")} + +
+ ); +}; + const UserAgentActivity: React.FC = ({ accessToken, userRole, dateValue, onDateChange }) => { // Maximum number of categories to show in charts to prevent color palette overflow const MAX_CATEGORIES = 10; @@ -78,6 +278,12 @@ const UserAgentActivity: React.FC = ({ accessToken, user const [selectedTags, setSelectedTags] = useState([]); const [tagsLoading, setTagsLoading] = useState(false); + // DEMO: Team filter state + const [selectedTeam, setSelectedTeam] = useState("All Teams"); + + // DEMO: Agent filter state (for filtering by demo agent names) + const [selectedAgents, setSelectedAgents] = useState([]); + // Separate loading states for each endpoint const [dauLoading, setDauLoading] = useState(false); const [wauLoading, setWauLoading] = useState(false); @@ -206,20 +412,14 @@ const UserAgentActivity: React.FC = ({ accessToken, user return () => clearTimeout(timeoutId); }, [accessToken, dateValue, selectedTags]); - // Helper function to extract user agent from tag + // Helper function to extract user agent from tag and clean it for display const extractUserAgent = (tag: string): string => { + let name = tag; if (tag.startsWith("User-Agent: ")) { - return tag.replace("User-Agent: ", ""); + name = tag.replace("User-Agent: ", ""); } - return tag; - }; - - // Helper function to truncate user agent name (used with Ant Design Tooltip) - const truncateUserAgent = (userAgent: string): string => { - if (userAgent.length > 15) { - return userAgent.substring(0, 15) + "..."; - } - return userAgent; + // Apply clean agent name mapping + return cleanAgentName(name); }; // Get all user agents for each chart type based on their specific data @@ -379,102 +579,151 @@ const UserAgentActivity: React.FC = ({ accessToken, user Performance metrics for different user agents
- {/* User Agent Filter */} -
- Filter by User Agents - + {DEMO_TEAMS_LIST.map((team) => ( + + {team} - ); - })} - + ))} + +
+ + {/* DEMO: User Agent Filter - Using hardcoded demo agents */} +
+ Filter by User Agents + +
{/* Date Range Picker is controlled by parent component */} - {/* Top 4 User Agents Cards */} + {/* DEMO: Top 5 User Agents Cards - Using hardcoded demo data */} {summaryLoading ? ( ) : ( - - {(summaryData.results || []).slice(0, 4).map((tag, index) => { - const userAgent = extractUserAgent(tag.tag); - const displayName = truncateUserAgent(userAgent); + + {DEMO_AGENTS.map((agent, index) => { return ( - - {displayName} - + {agent.name}
Success Requests - {formatAbbreviatedNumber(tag.successful_requests)} + {formatAbbreviatedNumber(agent.success_requests)}
Total Tokens - {formatAbbreviatedNumber(tag.total_tokens)} + {formatAbbreviatedNumber(agent.total_tokens)}
Total Cost - ${formatAbbreviatedNumber(tag.total_spend, 4)} + ${formatAbbreviatedNumber(agent.total_cost, 2)}
); })} - {/* Fill remaining slots if less than 4 agents */} - {Array.from({ length: Math.max(0, 4 - (summaryData.results || []).length) }).map((_, index) => ( - - No Data -
-
- Success Requests - - -
-
- Total Tokens - - -
-
- Total Cost - - -
-
-
- ))}
)} - {/* Main TabGroup for DAU/WAU/MAU vs Per User Usage */} + {/* DEMO: Main TabGroup - Per User Usage first, Per Team Usage second, then DAU/WAU/MAU */} + Per User Usage + Per Team Usage DAU/WAU/MAU - Per User Usage (Last 30 Days) - {/* DAU/WAU/MAU Tab Panel */} + {/* Per User Usage Tab Panel - First */} + + + + + {/* DEMO: Per Team Usage Tab Panel - Second (filtered by selected agents) */} + +
+ Per Team Usage + Usage metrics grouped by team +
+ + + + Team + Active Users + Total Requests + Total Tokens + Total Cost + + + + {computeTeamStats(DEMO_USERS_FOR_TEAMS, selectedAgents).map((team, index) => ( + setSelectedTeam(team.team)} + > + + {team.team} + + + {team.active_users} + + + {formatAbbreviatedNumber(team.total_requests)} + + + {formatAbbreviatedNumber(team.total_tokens)} + + + ${formatAbbreviatedNumber(team.total_cost, 2)} + + + ))} + +
+ Click a team row to filter the page by that team +
+ + {/* DAU/WAU/MAU Tab Panel - Third */}
DAU, WAU & MAU per Agent @@ -493,19 +742,17 @@ const UserAgentActivity: React.FC = ({ accessToken, user
Daily Active Users - Last 7 Days
- {dauLoading ? ( - - ) : ( - formatAbbreviatedNumber(value)} - yAxisWidth={60} - showLegend={true} - stack={true} - /> - )} + {/* DEMO: Using hardcoded DAU data instead of API data */} + formatAbbreviatedNumber(value)} + yAxisWidth={60} + showLegend={true} + stack={true} + /> @@ -548,15 +795,6 @@ const UserAgentActivity: React.FC = ({ accessToken, user - - {/* Per User Usage Tab Panel */} - - -