From 4c645c1ce36c40f5d2608c741bf91534f119d5a2 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 21:01:41 -0800 Subject: [PATCH 1/3] Endpoint usage POC --- .../EndpointUsage/EndpointUsage.tsx | 25 +++ .../components/EndpointUsageBarChart.tsx | 55 +++++ .../components/EndpointUsageLineChart.tsx | 212 ++++++++++++++++++ .../components/EndpointUsageTable.tsx | 122 ++++++++++ .../EndpointUsage/mockEndpointData.ts | 94 ++++++++ .../UsagePage/components/UsagePageView.tsx | 5 + .../src/components/UsagePage/types.ts | 1 + 7 files changed, 514 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/EndpointUsage.tsx create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageBarChart.tsx create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageLineChart.tsx create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageTable.tsx create mode 100644 ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/mockEndpointData.ts diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/EndpointUsage.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/EndpointUsage.tsx new file mode 100644 index 00000000000..b9e20f64b0b --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/EndpointUsage.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import EndpointUsageBarChart from "./components/EndpointUsageBarChart"; +import EndpointUsageLineChart from "./components/EndpointUsageLineChart"; +import EndpointUsageTable from "./components/EndpointUsageTable"; +import { mockEndpointData } from "./mockEndpointData"; +import { DailyData } from "../../types"; + +interface EndpointUsageProps { + userSpendData?: { + results: DailyData[]; + metadata: any; + }; +} + +const EndpointUsage: React.FC = ({ userSpendData }) => { + return ( +
+ + + +
+ ); +}; + +export default EndpointUsage; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageBarChart.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageBarChart.tsx new file mode 100644 index 00000000000..1ab3c49a7e6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageBarChart.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { BarChart, Card, Title } from "@tremor/react"; +import { CustomLegend, CustomTooltip } from "@/components/common_components/chartUtils"; +import { MetricWithMetadata } from "../../../types"; +import { mockEndpointData } from "../mockEndpointData"; + +interface EndpointUsageBarChartProps { + endpointData?: Record; +} + +const EndpointUsageBarChart: React.FC = ({ endpointData }) => { + // Use mock data if no endpointData is provided + const dataToUse = endpointData || mockEndpointData; + + // Transform endpoint data into chart format + const chartData = React.useMemo(() => { + return Object.entries(dataToUse).map(([endpoint, data]) => ({ + endpoint, + "metrics.successful_requests": data.metrics.successful_requests, + "metrics.failed_requests": data.metrics.failed_requests, + metrics: { + successful_requests: data.metrics.successful_requests, + failed_requests: data.metrics.failed_requests, + }, + })); + }, [dataToUse]); + + const valueFormatter = (value: number) => value.toLocaleString(); + + return ( + +
+ Success vs Failed Requests by Endpoint + +
+ +
+ ); +}; + +export default EndpointUsageBarChart; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageLineChart.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageLineChart.tsx new file mode 100644 index 00000000000..2bb897c89f6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageLineChart.tsx @@ -0,0 +1,212 @@ +import React, { useMemo, useState } from "react"; +import { Card, LineChart, Title } from "@tremor/react"; +import { Segmented } from "antd"; +import { BreakdownMetrics, DailyData, MetricWithMetadata } from "../../../types"; +import { mockEndpointData } from "../mockEndpointData"; + +interface EndpointUsageLineChartProps { + dailyData?: { results: DailyData[] }; + endpointData?: Record; +} + +type TimePeriod = "7" | "30" | "90"; + +// Create mock daily data from aggregated endpoint data +function createMockDailyData(endpointData: Record, days: number): DailyData[] { + const today = new Date(); + const dailyResults: DailyData[] = []; + + for (let i = days - 1; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + const dateStr = date.toISOString().split("T")[0]; + + // Distribute aggregated totals across days with some variation + const breakdown: { [key: string]: MetricWithMetadata } = {}; + Object.entries(endpointData).forEach(([endpointName, endpoint]) => { + // Calculate average daily values + const avgDaily = { + spend: endpoint.metrics.spend / days, + prompt_tokens: endpoint.metrics.prompt_tokens / days, + completion_tokens: endpoint.metrics.completion_tokens / days, + total_tokens: endpoint.metrics.total_tokens / days, + api_requests: endpoint.metrics.api_requests / days, + successful_requests: endpoint.metrics.successful_requests / days, + failed_requests: endpoint.metrics.failed_requests / days, + cache_read_input_tokens: endpoint.metrics.cache_read_input_tokens / days, + cache_creation_input_tokens: endpoint.metrics.cache_creation_input_tokens / days, + }; + + // Add simple variation (±20%) to make it look more realistic + const variation = 0.8 + Math.random() * 0.4; // Random factor between 0.8 and 1.2 + + breakdown[endpointName] = { + metrics: { + spend: avgDaily.spend * variation, + prompt_tokens: Math.round(avgDaily.prompt_tokens * variation), + completion_tokens: Math.round(avgDaily.completion_tokens * variation), + total_tokens: Math.round(avgDaily.total_tokens * variation), + api_requests: Math.round(avgDaily.api_requests * variation), + successful_requests: Math.round(avgDaily.successful_requests * variation), + failed_requests: Math.round(avgDaily.failed_requests * variation), + cache_read_input_tokens: Math.round(avgDaily.cache_read_input_tokens * variation), + cache_creation_input_tokens: Math.round(avgDaily.cache_creation_input_tokens * variation), + }, + metadata: endpoint.metadata, + api_key_breakdown: endpoint.api_key_breakdown, + }; + }); + + const breakdownMetrics: BreakdownMetrics = { + models: {}, + model_groups: {}, + mcp_servers: {}, + providers: {}, + api_keys: {}, + entities: {}, + endpoints: breakdown, + }; + + dailyResults.push({ + date: dateStr, + metrics: { + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: 0, + successful_requests: 0, + failed_requests: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + breakdown: breakdownMetrics, + }); + } + + return dailyResults; +} + +// Transform daily data into chart format +function transformDailyDataToChart(dailyData: DailyData[], days: number): Array> { + const chartData: Array> = []; + + // Get all unique endpoint names + const endpointNames = new Set(); + dailyData.forEach((day) => { + if (day.breakdown.endpoints) { + Object.keys(day.breakdown.endpoints).forEach((name) => endpointNames.add(name)); + } + }); + + // Filter to the last N days based on time period + const filteredData = dailyData.slice(-days); + + filteredData.forEach((day) => { + const date = new Date(day.date); + const dateStr = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + + const dataPoint: Record = { + date: dateStr, + }; + + endpointNames.forEach((endpointName) => { + const endpoint = day.breakdown.endpoints?.[endpointName]; + dataPoint[endpointName] = endpoint?.metrics.api_requests || 0; + }); + + chartData.push(dataPoint); + }); + + return chartData; +} + +export function EndpointUsageLineChart({ dailyData, endpointData }: EndpointUsageLineChartProps) { + const [timePeriod, setTimePeriod] = useState("7"); + const days = parseInt(timePeriod); + + // Use daily data if provided and has endpoints, otherwise create mock daily data from aggregated endpoint data + const chartData = useMemo(() => { + // Check if dailyData has endpoints breakdown + const hasEndpointsInDailyData = + dailyData?.results && + dailyData.results.some((day) => day.breakdown.endpoints && Object.keys(day.breakdown.endpoints).length > 0); + + if (hasEndpointsInDailyData) { + // Check if we have a reasonable number of endpoints (at least 2 for proof of concept) + const endpointCount = new Set(); + dailyData.results.forEach((day) => { + if (day.breakdown.endpoints) { + Object.keys(day.breakdown.endpoints).forEach((name) => endpointCount.add(name)); + } + }); + + // If we have multiple endpoints in real data, use it; otherwise use mock data for proof of concept + if (endpointCount.size >= 2) { + return transformDailyDataToChart(dailyData.results, days); + } + } + + // Use mock data for proof of concept + const dataToUse = endpointData || mockEndpointData; + const mockDaily = createMockDailyData(dataToUse, days); + return transformDailyDataToChart(mockDaily, days); + }, [dailyData, endpointData, days]); + + // Get endpoint names from chart data + const categories = useMemo(() => { + if (chartData.length === 0) return []; + const keys = Object.keys(chartData[0]).filter((key) => key !== "date"); + return keys; + }, [chartData]); + + // Tremor color palette for multiple lines + const colors = ["blue", "cyan", "indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange"]; + + return ( + +
+ Endpoint Usage Trends + setTimePeriod(value as TimePeriod)} + style={{ + backgroundColor: "#f3f4f6", + }} + /> +
+ value.toLocaleString()} + showLegend={true} + showGridLines={true} + yAxisWidth={60} + connectNulls={true} + curveType="natural" + /> +
+ ); +} + +export default EndpointUsageLineChart; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageTable.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageTable.tsx new file mode 100644 index 00000000000..f5cfb553370 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/components/EndpointUsageTable.tsx @@ -0,0 +1,122 @@ +import React from "react"; +import { Table, Progress } from "antd"; +import type { ColumnsType } from "antd/es/table"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { MetricWithMetadata } from "../../../types"; + +interface EndpointUsageTableProps { + endpointData: Record; +} + +interface EndpointRow { + key: string; + endpoint: string; + successful_requests: number; + failed_requests: number; + api_requests: number; + total_tokens: number; + spend: number; + successRate: number; +} + +const EndpointUsageTable: React.FC = ({ endpointData }) => { + const calculateSuccessRate = (successful: number, total: number): number => { + if (total === 0) return 0; + return (successful / total) * 100; + }; + + const dataSource: EndpointRow[] = Object.entries(endpointData).map(([endpoint, data]) => ({ + key: endpoint, + endpoint, + successful_requests: data.metrics.successful_requests, + failed_requests: data.metrics.failed_requests, + api_requests: data.metrics.api_requests, + total_tokens: data.metrics.total_tokens, + spend: data.metrics.spend, + successRate: calculateSuccessRate(data.metrics.successful_requests, data.metrics.api_requests), + })); + + const columns: ColumnsType = [ + { + title: "Endpoint", + dataIndex: "endpoint", + key: "endpoint", + render: (text: string) => {text}, + }, + { + title: "Successful / Failed", + key: "requests", + render: (_: any, record: EndpointRow) => { + const successPercentage = + record.api_requests > 0 ? (record.successful_requests / record.api_requests) * 100 : 0; + const failurePercentage = record.api_requests > 0 ? (record.failed_requests / record.api_requests) * 100 : 0; + const totalPercentage = successPercentage + failurePercentage; + + const strokeColorConfig: Record = { + "0%": "#22c55e", + }; + if (successPercentage > 0 && successPercentage < 100) { + strokeColorConfig[`${successPercentage}%`] = "#22c55e"; + strokeColorConfig[`${successPercentage + 0.01}%`] = "#ef4444"; + } + strokeColorConfig["100%"] = failurePercentage > 0 ? "#ef4444" : "#22c55e"; + + return ( +
+
+ +
+
+ {record.successful_requests.toLocaleString()} + / + {record.failed_requests.toLocaleString()} +
+
+ ); + }, + }, + { + title: "Total Request", + dataIndex: "api_requests", + key: "api_requests", + render: (value: number) => value.toLocaleString(), + }, + { + title: "Success Rate", + dataIndex: "successRate", + key: "successRate", + render: (value: number) => { + const successRateStr = value.toFixed(2); + return ( + = 95 + ? "text-green-600 font-medium" + : value >= 80 + ? "text-yellow-600 font-medium" + : "text-red-600 font-medium" + } + > + {successRateStr}% + + ); + }, + }, + { + title: "Total Tokens", + dataIndex: "total_tokens", + key: "total_tokens", + render: (value: number) => value.toLocaleString(), + }, + { + title: "Spend", + dataIndex: "spend", + key: "spend", + render: (value: number) => `$${formatNumberWithCommas(value, 2)}`, + }, + ]; + + return ; +}; + +export default EndpointUsageTable; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/mockEndpointData.ts b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/mockEndpointData.ts new file mode 100644 index 00000000000..cb5424dde69 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/EndpointUsage/mockEndpointData.ts @@ -0,0 +1,94 @@ +import { MetricWithMetadata } from "../../types"; + +export const mockEndpointData: Record = { + "/chat/completions": { + metrics: { + spend: 1250.45, + prompt_tokens: 150000, + completion_tokens: 45000, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + total_tokens: 195000, + successful_requests: 1250, + failed_requests: 45, + api_requests: 1295, + }, + metadata: {}, + api_key_breakdown: {}, + }, + "/audio/speech": { + metrics: { + spend: 320.78, + prompt_tokens: 0, + completion_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + total_tokens: 0, + successful_requests: 890, + failed_requests: 12, + api_requests: 902, + }, + metadata: {}, + api_key_breakdown: {}, + }, + "/audio/transcriptions": { + metrics: { + spend: 85.23, + prompt_tokens: 0, + completion_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + total_tokens: 0, + successful_requests: 234, + failed_requests: 8, + api_requests: 242, + }, + metadata: {}, + api_key_breakdown: {}, + }, + "/embeddings": { + metrics: { + spend: 450.12, + prompt_tokens: 200000, + completion_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + total_tokens: 200000, + successful_requests: 2100, + failed_requests: 23, + api_requests: 2123, + }, + metadata: {}, + api_key_breakdown: {}, + }, + "/images/generations": { + metrics: { + spend: 180.56, + prompt_tokens: 0, + completion_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + total_tokens: 0, + successful_requests: 156, + failed_requests: 5, + api_requests: 161, + }, + metadata: {}, + api_key_breakdown: {}, + }, + "/moderations": { + metrics: { + spend: 95.34, + prompt_tokens: 50000, + completion_tokens: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + total_tokens: 50000, + successful_requests: 567, + failed_requests: 3, + api_requests: 570, + }, + metadata: {}, + api_key_breakdown: {}, + }, +}; diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx index a9c86c064ef..01ad2c7b466 100644 --- a/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx +++ b/ui/litellm-dashboard/src/components/UsagePage/components/UsagePageView.tsx @@ -53,6 +53,7 @@ import EntityUsage, { EntityList } from "./EntityUsage/EntityUsage"; import TopKeyView from "./EntityUsage/TopKeyView"; import { UsageOption, UsageViewSelect } from "./UsageViewSelect/UsageViewSelect"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; +import EndpointUsage from "./EndpointUsage/EndpointUsage"; interface UsagePageProps { teams: Team[]; @@ -439,6 +440,7 @@ const UsagePage: React.FC = ({ teams, organizations }) => { Model Activity Key Activity MCP Server Activity + Endpoint Activity