diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index bb3402f260..bda369e8d3 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1653,6 +1653,14 @@ async def ui_view_spend_logs( # noqa: PLR0915 page_size: int = fastapi.Query( default=50, description="Number of items per page", ge=1, le=100 ), + status_filter: Optional[str] = fastapi.Query( + default=None, + description="Filter logs by status (e.g., success, failure)" + ), + model: Optional[str] = fastapi.Query( # Add this new parameter + default=None, + description="Filter logs by model name" + ), user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), ): """ @@ -1684,7 +1692,6 @@ async def ui_view_spend_logs( # noqa: PLR0915 param="None", code=status.HTTP_400_BAD_REQUEST, ) - try: # Convert the date strings to datetime objects start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S").replace( @@ -1699,28 +1706,24 @@ async def ui_view_spend_logs( # noqa: PLR0915 end_date_iso = end_date_obj.isoformat() # Already in UTC, no need to add Z # Build where conditions - where_conditions: dict[str, Any] = { - "startTime": {"gte": start_date_iso, "lte": end_date_iso}, + where_conditions: Dict[str, Any] = { + "startTime": {"gte": start_date_iso, "lte": end_date_iso} # Ensure date range is always applied } - - if team_id is not None: - where_conditions["team_id"] = team_id - - if api_key is not None: + if api_key: where_conditions["api_key"] = api_key - - if user_id is not None: - where_conditions["user"] = user_id - - if request_id is not None: + if user_id: + where_conditions["user"] = user_id + if request_id: where_conditions["request_id"] = request_id + if team_id: + where_conditions["team_id"] = team_id + if model: + where_conditions["model"] = model + if min_spend is not None: + where_conditions.setdefault("spend", {}).update({"gte": min_spend}) + if max_spend is not None: + where_conditions.setdefault("spend", {}).update({"lte": max_spend}) - if min_spend is not None or max_spend is not None: - where_conditions["spend"] = {} - if min_spend is not None: - where_conditions["spend"]["gte"] = min_spend - if max_spend is not None: - where_conditions["spend"]["lte"] = max_spend # Calculate skip value for pagination skip = (page - 1) * page_size diff --git a/ui/litellm-dashboard/src/components/common_components/filter.tsx b/ui/litellm-dashboard/src/components/common_components/filter.tsx index 1155a04192..eae3413769 100644 --- a/ui/litellm-dashboard/src/components/common_components/filter.tsx +++ b/ui/litellm-dashboard/src/components/common_components/filter.tsx @@ -8,6 +8,7 @@ export interface FilterOption { label?: string; isSearchable?: boolean; searchFn?: (searchText: string) => Promise>; + options?: Array<{ label: string; value: string }>; } interface FilterValues { @@ -74,12 +75,13 @@ const FilterComponent: React.FC = ({ // Define the order of filters const orderedFilters = [ 'Team ID', + 'Status', + 'Model', 'Organization ID', 'Key Alias', 'User ID', 'Key Hash' ]; - return (
@@ -122,6 +124,20 @@ const FilterComponent: React.FC = ({ options={searchOptionsMap[option.name] || []} allowClear /> + ) : option.options ? ( + ) : ( { try { // Construct base URL @@ -2162,6 +2164,8 @@ export const uiSpendLogsCall = async ( if (page) queryParams.append('page', page.toString()); if (page_size) queryParams.append('page_size', page_size.toString()); if (user_id) queryParams.append('user_id', user_id); + if (status_filter) queryParams.append('status_filter', status_filter); + if (model) queryParams.append('model', model); // Append query parameters to URL if any exist const queryString = queryParams.toString(); diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 9fab24c9a2..944abdb76a 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -74,16 +74,20 @@ export default function SpendLogsTable({ allOrganizations, allUsers, allKeyAliases, + allModels, handleFilterChange, handleFilterReset, isLoading, pagination, setCurrentPage, + handleRefresh, } = useLogFilterLogic({ accessToken, startTime, endTime, pageSize, + userID, + userRole, }); const sessionLogs = useQuery<{data: LogEntry[]}>({ @@ -169,10 +173,6 @@ export default function SpendLogsTable({ onSessionClick: (sessionId: string) => {}, })) || []; - const handleRefresh = () => { - queryClient.invalidateQueries({ queryKey: ['logs', 'table', accessToken, startTime, endTime, filters] }); - }; - const getTimeRangeDisplay = () => { if (isCustomDate) { return `${moment(startTime).format('MMM D, h:mm A')} - ${moment(endTime).format('MMM D, h:mm A')}`; @@ -204,7 +204,6 @@ export default function SpendLogsTable({ searchFn: async (searchText: string) => { if (!hookAllTeams || hookAllTeams.length === 0) return []; const filtered = hookAllTeams.filter((team: Team) =>{ - console.log("team", searchText) return team.team_id.toLowerCase().includes(searchText.toLowerCase()) || (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) }); @@ -214,55 +213,32 @@ export default function SpendLogsTable({ })); } }, - // { - // name: 'Key Alias', - // label: 'Key Alias', - // isSearchable: true, - // searchFn: async (searchText: string) => { - // const filteredKeyAliases = allKeyAliases.filter(key => { - // console.log("key", searchText); - // return key.toLowerCase().includes(searchText.toLowerCase()) - // }); - - // return filteredKeyAliases.map((key) => { - // return { - // label: key, - // value: key - // } - // }); - // } - // }, - // { - // name: 'Key Hash', - // label: 'Key Hash', - // isSearchable: false, - // }, - // { - // name: 'Request ID', - // label: 'Request ID', - // isSearchable: false, - // }, - // { - // name: 'Model', - // label: 'Model', - // isSearchable: false, - // }, - // { - // name: 'User', - // label: 'User', - // isSearchable: true, - // searchFn: async (searchText: string) => { - // if (!allUsers || allUsers.length === 0) return []; - // const filtered = allUsers.filter((user: UserInfo) => - // (user.user_id && user.user_id.toLowerCase().includes(searchText.toLowerCase())) || - // (user.user_email && user.user_email.toLowerCase().includes(searchText.toLowerCase())) - // ); - // return filtered.map((user: UserInfo) => ({ - // label: `${user.user_email || user.user_id} (${user.user_id})`, - // value: user.user_id - // })); - // } - // }, + { + name: 'Status', + label: 'Status', + isSearchable: false, + options: [ + { label: 'All', value: '' }, + { label: 'Success', value: 'success' }, + { label: 'Failure', value: 'failure' }, + ], + }, + { + name: 'Model', + label: 'Model', + isSearchable: true, + searchFn: async (searchText: string) => { + // Get unique models from the current logs + const uniqueModels = new Set(filteredLogs.map((log: LogEntry) => log.model)); + const filteredModels = Array.from(uniqueModels).filter(model => + model.toLowerCase().includes(searchText.toLowerCase()) + ); + return filteredModels.map(model => ({ + label: model, + value: model + })); + } + }, ]; if (selectedSessionId && sessionLogs.data) { @@ -501,7 +477,7 @@ export default function SpendLogsTable({ } return ( -
+

Request Logs @@ -625,6 +601,62 @@ export default function SpendLogsTable({ Refresh + +
+ + Showing{" "} + {isLoading + ? "..." + : pagination.totalCount > 0 + ? (pagination.currentPage - 1) * pagination.pageSize + 1 + : 0}{" "} + -{" "} + {isLoading + ? "..." + : pagination.totalCount > 0 + ? Math.min(pagination.currentPage * pagination.pageSize, pagination.totalCount) + : 0}{" "} + of{" "} + {isLoading + ? "..." + : pagination.totalCount ?? 0}{" "} + results + +
+ + Page {isLoading ? "..." : pagination.currentPage} of{" "} + {isLoading + ? "..." + : pagination.totalPages ?? 1} + + + +
+

{isCustomDate && ( diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx index 0a04e161ea..60cdf8dedb 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useState, useRef, useMemo } from "react"; import { LogEntry } from "./columns"; -import { uiSpendLogsCall, Organization, Team, UserInfo, teamListCall, userListCall, keyListCall as fetchAllKeysCall } from "../networking"; +import { uiSpendLogsCall, Organization, Team, UserInfo, teamListCall, userListCall, keyListCall as fetchAllKeysCall, modelAvailableCall } from "../networking"; import { KeyResponse } from "../key_team_helpers/key_list"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Setter } from "@/types"; @@ -37,7 +37,9 @@ export function useLogFilterLogic({ endTime, // Receive from SpendLogsTable pageSize = defaultPageSize, initialPage = 1, - initialFilters = {} + initialFilters = {}, + userID, + userRole }: { accessToken: string | null; startTime: string; @@ -45,6 +47,8 @@ export function useLogFilterLogic({ pageSize?: number; initialPage?: number; initialFilters?: Partial; + userID: string | null; + userRole: string | null; }) { const defaultFilters: LogFilterState = { 'Team ID': '', @@ -109,6 +113,24 @@ export function useLogFilterLogic({ }, enabled: !!accessToken, }); + + const { data: allModels = [] } = useQuery({ + queryKey: ['allModels', accessToken, userID, userRole], + queryFn: async () => { + if (!accessToken || !userID || !userRole) return []; + + const response = await modelAvailableCall( + accessToken, + userID, + userRole, + false, // return_wildcard_routes + null // teamID + ); + + return response.data.map((model: { id: string }) => model.id); + }, + enabled: !!accessToken && !!userID && !!userRole, + }); // Debounced API call const debouncedSearch = useCallback( @@ -134,7 +156,8 @@ export function useLogFilterLogic({ const teamIdParam = currentFilters['Team ID'] || undefined; const requestIdParam = currentFilters['Request ID'] || undefined; const userIdParam = currentFilters['User'] || undefined; - // const modelParam = currentFilters['Model'] || undefined; // Prepared if uiSpendLogsCall is updated + const statusParam = currentFilters['Status'] || undefined; + const modelParam = currentFilters['Model'] || undefined; const response = await uiSpendLogsCall( accessToken, @@ -145,9 +168,9 @@ export function useLogFilterLogic({ formattedEndTime, pageToFetch, pageSize, - userIdParam - // If uiSpendLogsCall is updated to accept more params (e.g., model), pass them here: - // modelParam, + userIdParam, + statusParam, + modelParam, ); if (currentTimestamp === lastSearchTimestamp.current) { @@ -226,6 +249,13 @@ export function useLogFilterLogic({ pageSize: paginationDetails.pageSize, }; + const handleRefresh = () => { + // Reset to first page + setCurrentPage(1); + // The useEffect in useLogFilterLogic will automatically trigger a refetch + // when currentPage changes + }; + return { filters, filteredLogs: logEntries, @@ -233,11 +263,13 @@ export function useLogFilterLogic({ allTeams: allTeams || [], allUsers: allUsers || [], allOrganizations: [], // Placeholder for now, can be fetched if needed + allModels: allModels || [], handleFilterChange, handleFilterReset, isLoading: isLoadingLogs, pagination, setCurrentPage, - error: logsError + error: logsError, + handleRefresh }; } \ No newline at end of file