diff --git a/litellm/proxy/proxy_config.yaml b/litellm/proxy/proxy_config.yaml index 65b04a6c84..868aec66ce 100644 --- a/litellm/proxy/proxy_config.yaml +++ b/litellm/proxy/proxy_config.yaml @@ -11,7 +11,11 @@ model_list: api_key: os.environ/ANTHROPIC_API_KEY model_info: health_check_model: anthropic/claude-3-5-sonnet-20240620 + - model_name: fake-openai-endpoint + litellm_params: + model: openai/fake + api_key: fake-key + api_base: https://exampleopenaiendpoint-production.up.railway.app/ general_settings: store_prompts_in_spend_logs: true - diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 1dae6bffed..e63bef71ee 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1638,10 +1638,25 @@ async def ui_view_spend_logs( # noqa: PLR0915 default=None, description="Time till which to view key spend", ), + page: int = fastapi.Query( + default=1, description="Page number for pagination", ge=1 + ), + page_size: int = fastapi.Query( + default=50, description="Number of items per page", ge=1, le=100 + ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ - View spend logs for UI + View spend logs for UI with pagination support + + Returns: + { + "data": List[LiteLLM_SpendLogs], # Paginated spend logs + "total": int, # Total number of records + "page": int, # Current page number + "page_size": int, # Number of items per page + "total_pages": int # Total number of pages + } """ from litellm.proxy.proxy_server import prisma_client @@ -1656,7 +1671,13 @@ async def ui_view_spend_logs( # noqa: PLR0915 verbose_proxy_logger.debug( "Prompts and responses are not stored in spend logs, returning empty list" ) - return [] + return { + "data": [], + "total": 0, + "page": page, + "page_size": page_size, + "total_pages": 0, + } if start_date is None or end_date is None: raise ProxyException( message="Start date and end date are required", @@ -1672,15 +1693,39 @@ async def ui_view_spend_logs( # noqa: PLR0915 start_date_iso = start_date_obj.isoformat() + "Z" # Add Z to indicate UTC end_date_iso = end_date_obj.isoformat() + "Z" # Add Z to indicate UTC - return await prisma_client.db.litellm_spendlogs.find_many( + # Calculate skip value for pagination + skip = (page - 1) * page_size + + # Get total count of records + total_records = await prisma_client.db.litellm_spendlogs.count( + where={ + "startTime": {"gte": start_date_iso, "lte": end_date_iso}, + } + ) + + # Get paginated data + data = await prisma_client.db.litellm_spendlogs.find_many( where={ "startTime": {"gte": start_date_iso, "lte": end_date_iso}, }, order={ "startTime": "desc", }, + skip=skip, + take=page_size, ) + # Calculate total pages + total_pages = (total_records + page_size - 1) // page_size + + return { + "data": data, + "total": total_records, + "page": page, + "page_size": page_size, + "total_pages": total_pages, + } + @router.get( "/spend/logs", diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 30a84f4760..f219b563f9 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1513,6 +1513,7 @@ export const userSpendLogsCall = async ( throw error; } }; + export const uiSpendLogsCall = async ( accessToken: String, api_key?: string, @@ -1520,6 +1521,8 @@ export const uiSpendLogsCall = async ( request_id?: string, start_date?: string, end_date?: string, + page?: number, + page_size?: number, ) => { try { // Construct base URL @@ -1532,6 +1535,9 @@ export const uiSpendLogsCall = async ( if (request_id) queryParams.append('request_id', request_id); if (start_date) queryParams.append('start_date', start_date); if (end_date) queryParams.append('end_date', end_date); + if (page) queryParams.append('page', page.toString()); + if (page_size) queryParams.append('page_size', page_size.toString()); + // Append query parameters to URL if any exist const queryString = queryParams.toString(); if (queryString) { diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 0d889d0ec6..213c89aeac 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -56,13 +56,25 @@ export const columns: ColumnDef[] = [ header: "Request ID", accessorKey: "request_id", cell: (info: any) => ( - {String(info.getValue() || "")} + + {String(info.getValue() || "")} + ), }, { - header: "Type", - accessorKey: "call_type", - cell: (info: any) => {String(info.getValue() || "")}, + header: "Country", + accessorKey: "requester_ip_address", + cell: (info: any) => , + }, + { + header: "Team", + accessorKey: "metadata.user_api_key_team_alias", + cell: (info: any) => {String(info.getValue() || "-")}, + }, + { + header: "Key Name", + accessorKey: "metadata.user_api_key_alias", + cell: (info: any) => {String(info.getValue() || "-")}, }, { header: "Request", diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 2d7c4a672c..27ccf5e12a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -1,5 +1,6 @@ import moment from "moment"; import { useQuery } from "@tanstack/react-query"; +import { useState, useRef, useEffect } from "react"; import { uiSpendLogsCall } from "../networking"; import { DataTable } from "./table"; @@ -13,39 +14,92 @@ interface SpendLogsTableProps { userID: string | null; } +interface PaginatedResponse { + data: LogEntry[]; + total: number; + page: number; + page_size: number; + total_pages: number; +} + export default function SpendLogsTable({ accessToken, token, userRole, userID, }: SpendLogsTableProps) { - const logs = useQuery({ - queryKey: ["logs", "table"], + const [searchTerm, setSearchTerm] = useState(""); + const [keyNameFilter, setKeyNameFilter] = useState(""); + const [teamNameFilter, setTeamNameFilter] = useState(""); + const [showFilters, setShowFilters] = useState(false); + const [showColumnDropdown, setShowColumnDropdown] = useState(false); + const [selectedColumn, setSelectedColumn] = useState("Key Name"); + const [currentPage, setCurrentPage] = useState(1); + const [pageSize] = useState(50); + const dropdownRef = useRef(null); + + // New state variables for Start and End Time + const [startTime, setStartTime] = useState( + moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm") + ); + const [endTime, setEndTime] = useState( + moment().format("YYYY-MM-DDTHH:mm") + ); + + // Add these new state variables at the top with other useState declarations + const [isCustomDate, setIsCustomDate] = useState(false); + const [quickSelectOpen, setQuickSelectOpen] = useState(false); + + // Close dropdown when clicking outside + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setShowColumnDropdown(false); + } + } + + document.addEventListener("mousedown", handleClickOutside); + return () => + document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const logs = useQuery({ + queryKey: ["logs", "table", currentPage, pageSize, startTime, endTime], queryFn: async () => { if (!accessToken || !token || !userRole || !userID) { console.log( "got None values for one of accessToken, token, userRole, userID", ); - return; + return { + data: [], + total: 0, + page: 1, + page_size: pageSize, + total_pages: 0, + }; } - // Get logs for last 24 hours using ISO string and proper date formatting - const endTime = moment().format("YYYY-MM-DD HH:mm:ss"); - const startTime = moment() - .subtract(24, "hours") - .format("YYYY-MM-DD HH:mm:ss"); + const formattedStartTime = moment(startTime).format("YYYY-MM-DD HH:mm:ss"); + const formattedEndTime = moment(endTime).format("YYYY-MM-DD HH:mm:ss"); const data = await uiSpendLogsCall( accessToken, token, userRole, userID, - startTime, - endTime, + formattedStartTime, + formattedEndTime, + currentPage, + pageSize, ); return data; }, + // Refetch when startTime or endTime changes + enabled: !!accessToken && !!token && !!userRole && !!userID, }); if (!accessToken || !token || !userRole || !userID) { @@ -55,36 +109,326 @@ export default function SpendLogsTable({ return null; } + const filteredData = + logs.data?.data?.filter((log) => { + const matchesSearch = + !searchTerm || + log.request_id.includes(searchTerm) || + log.model.includes(searchTerm) || + (log.user && log.user.includes(searchTerm)); + const matchesKeyName = + !keyNameFilter || + (log.metadata?.user_api_key_alias && + log.metadata.user_api_key_alias + .toLowerCase() + .includes(keyNameFilter.toLowerCase())); + const matchesTeamName = + !teamNameFilter || + (log.metadata?.user_api_key_team_alias && + log.metadata.user_api_key_team_alias + .toLowerCase() + .includes(teamNameFilter.toLowerCase())); + return matchesSearch && matchesKeyName && matchesTeamName; + }) || []; + return ( -
+

Traces

-
-
- - - -
-
- +
+
+
+
+ setSearchTerm(e.target.value)} + /> + + + +
+
+ + + {showFilters && ( +
+
+ Where +
+ + {showColumnDropdown && ( +
+ {["Key Name", "Team Name"].map((option) => ( + + ))} +
+ )} +
+ { + if (selectedColumn === "Key Name") { + setKeyNameFilter(e.target.value); + } else { + setTeamNameFilter(e.target.value); + } + }} + /> + +
+
+ )} +
+ +
+ + + {quickSelectOpen && ( +
+
+ {[ + { label: "Last 15 Minutes", value: 15, unit: "minutes" }, + { label: "Last Hour", value: 1, unit: "hours" }, + { label: "Last 4 Hours", value: 4, unit: "hours" }, + { label: "Last 24 Hours", value: 24, unit: "hours" }, + { label: "Last 7 Days", value: 7, unit: "days" }, + ].map((option) => ( + + ))} +
+ +
+
+ )} +
+ + {isCustomDate && ( +
+
+ { + setStartTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ to +
+ { + setEndTime(e.target.value); + setCurrentPage(1); + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ )} +
+ +
+ + Showing{" "} + {logs.isLoading + ? "..." + : logs.data + ? (currentPage - 1) * pageSize + 1 + : 0}{" "} + -{" "} + {logs.isLoading + ? "..." + : logs.data + ? Math.min(currentPage * pageSize, logs.data.total) + : 0}{" "} + of{" "} + {logs.isLoading + ? "..." + : logs.data + ? logs.data.total + : 0}{" "} + results + +
+ + Page {logs.isLoading ? "..." : currentPage} of{" "} + {logs.isLoading + ? "..." + : logs.data + ? logs.data.total_pages + : 1} + + + +
+
true} /> @@ -121,7 +465,7 @@ function RequestViewer({ row }: { row: Row }) {
-          {JSON.stringify(formatData(row.original.request_id), null, 2)}
+          {JSON.stringify(formatData(row.original.messages), null, 2)}