diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index bf8cbaef1d..63489f3f0e 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1627,7 +1627,19 @@ async def ui_view_spend_logs( # noqa: PLR0915 ), request_id: Optional[str] = fastapi.Query( default=None, - description="request_id to get spend logs for specific request_id. If none passed then pass spend logs for all requests", + description="request_id to get spend logs for specific request_id", + ), + team_id: Optional[str] = fastapi.Query( + default=None, + description="Filter spend logs by team_id", + ), + min_spend: Optional[float] = fastapi.Query( + default=None, + description="Filter logs with spend greater than or equal to this value", + ), + max_spend: Optional[float] = fastapi.Query( + default=None, + description="Filter logs with spend less than or equal to this value", ), start_date: Optional[str] = fastapi.Query( default=None, @@ -1674,46 +1686,70 @@ async def ui_view_spend_logs( # noqa: PLR0915 param="None", code=status.HTTP_400_BAD_REQUEST, ) - # Convert the date strings to datetime objects - start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") - end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S") - # Convert to ISO format strings for Prisma - 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 + try: + # Convert the date strings to datetime objects + start_date_obj = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S") + end_date_obj = datetime.strptime(end_date, "%Y-%m-%d %H:%M:%S") - # Calculate skip value for pagination - skip = (page - 1) * page_size + # Convert to ISO format strings for Prisma + 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 - # Get total count of records - total_records = await prisma_client.db.litellm_spendlogs.count( - where={ + # Build where conditions + where_conditions: dict[str, Any] = { "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, - ) + if team_id is not None: + where_conditions["team_id"] = team_id - # Calculate total pages - total_pages = (total_records + page_size - 1) // page_size + if api_key is not None: + where_conditions["api_key"] = api_key - return { - "data": data, - "total": total_records, - "page": page, - "page_size": page_size, - "total_pages": total_pages, - } + if user_id is not None: + where_conditions["user"] = user_id + + if request_id is not None: + where_conditions["request_id"] = request_id + + 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 + + # Get total count of records + total_records = await prisma_client.db.litellm_spendlogs.count( + where=where_conditions, + ) + + # Get paginated data + data = await prisma_client.db.litellm_spendlogs.find_many( + where=where_conditions, + 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, + } + except Exception as e: + verbose_proxy_logger.exception(f"Error in ui_view_spend_logs: {e}") + raise handle_exception_on_proxy(e) @router.get( diff --git a/tests/local_testing/test_stream_chunk_builder.py b/tests/local_testing/test_stream_chunk_builder.py index eba858ca00..8e7cfcf9ed 100644 --- a/tests/local_testing/test_stream_chunk_builder.py +++ b/tests/local_testing/test_stream_chunk_builder.py @@ -721,22 +721,14 @@ def test_stream_chunk_builder_openai_audio_output_usage(): print(f"response usage: {response.usage}") check_non_streaming_response(response) print(f"response: {response}") - for k, v in usage_obj.model_dump(exclude_none=True).items(): - print(k, v) - response_usage_value = getattr(response.usage, k) # type: ignore - print(f"response_usage_value: {response_usage_value}") - print(f"type: {type(response_usage_value)}") - if isinstance(response_usage_value, BaseModel): - response_usage_value_dict = response_usage_value.model_dump( - exclude_none=True - ) - if isinstance(v, dict): - for key, value in v.items(): - assert response_usage_value_dict[key] == value - else: - assert response_usage_value_dict == v - else: - assert response_usage_value == v + # Convert both usage objects to dictionaries for easier comparison + usage_dict = usage_obj.model_dump(exclude_none=True) + response_usage_dict = response.usage.model_dump(exclude_none=True) + + # Simple dictionary comparison + assert ( + usage_dict == response_usage_dict + ), f"\nExpected: {usage_dict}\nGot: {response_usage_dict}" def test_stream_chunk_builder_empty_initial_chunk(): diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 8b2df5ac5a..6869bcdc58 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -1553,12 +1553,14 @@ export const userSpendLogsCall = async ( export const uiSpendLogsCall = async ( accessToken: String, api_key?: string, - user_id?: string, + team_id?: string, request_id?: string, start_date?: string, end_date?: string, page?: number, page_size?: number, + min_spend?: number, + max_spend?: number, ) => { try { // Construct base URL @@ -1567,7 +1569,9 @@ export const uiSpendLogsCall = async ( // Add query parameters if they exist const queryParams = new URLSearchParams(); if (api_key) queryParams.append('api_key', api_key); - if (user_id) queryParams.append('user_id', user_id); + if (team_id) queryParams.append('team_id', team_id); + if (min_spend) queryParams.append('min_spend', min_spend.toString()); + if (max_spend) queryParams.append('max_spend', max_spend.toString()); 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); @@ -1595,7 +1599,7 @@ export const uiSpendLogsCall = async ( } const data = await response.json(); - console.log("Spend Logs UI Response:", data); + console.log("Spend Logs Response:", data); return data; } catch (error) { console.error("Failed to fetch spend logs:", error); diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 213c89aeac..00fa117584 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -7,6 +7,7 @@ import { CountryCell } from "./country_cell"; export type LogEntry = { request_id: string; api_key: string; + team_id: string; model: string; api_base?: string; call_type: string; @@ -155,11 +156,6 @@ export const columns: ColumnDef[] = [ ); }, }, - { - header: "Country", - accessorKey: "requester_ip_address", - cell: (info: any) => , - }, ]; const formatMessage = (message: any): string => { diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 27ccf5e12a..918c4afad4 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -29,14 +29,13 @@ export default function SpendLogsTable({ userID, }: SpendLogsTableProps) { 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); + const filtersRef = useRef(null); + const quickSelectRef = useRef(null); // New state variables for Start and End Time const [startTime, setStartTime] = useState( @@ -49,6 +48,11 @@ export default function SpendLogsTable({ // Add these new state variables at the top with other useState declarations const [isCustomDate, setIsCustomDate] = useState(false); const [quickSelectOpen, setQuickSelectOpen] = useState(false); + const [tempTeamId, setTempTeamId] = useState(""); + const [tempKeyHash, setTempKeyHash] = useState(""); + const [selectedTeamId, setSelectedTeamId] = useState(""); + const [selectedKeyHash, setSelectedKeyHash] = useState(""); + const [selectedFilter, setSelectedFilter] = useState("Team ID"); // Close dropdown when clicking outside useEffect(() => { @@ -59,6 +63,18 @@ export default function SpendLogsTable({ ) { setShowColumnDropdown(false); } + if ( + filtersRef.current && + !filtersRef.current.contains(event.target as Node) + ) { + setShowFilters(false); + } + if ( + quickSelectRef.current && + !quickSelectRef.current.contains(event.target as Node) + ) { + setQuickSelectOpen(false); + } } document.addEventListener("mousedown", handleClickOutside); @@ -67,12 +83,19 @@ export default function SpendLogsTable({ }, []); const logs = useQuery({ - queryKey: ["logs", "table", currentPage, pageSize, startTime, endTime], + queryKey: [ + "logs", + "table", + currentPage, + pageSize, + startTime, + endTime, + selectedTeamId, + selectedKeyHash, + ], queryFn: async () => { if (!accessToken || !token || !userRole || !userID) { - console.log( - "got None values for one of accessToken, token, userRole, userID", - ); + console.log("Missing required auth parameters"); return { data: [], total: 0, @@ -85,20 +108,17 @@ export default function SpendLogsTable({ 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( + return await uiSpendLogsCall( accessToken, - token, - userRole, - userID, + selectedKeyHash || undefined, + selectedTeamId || undefined, + undefined, // This parameter might be setting a default min_spend formattedStartTime, formattedEndTime, currentPage, - pageSize, + pageSize ); - - return data; }, - // Refetch when startTime or endTime changes enabled: !!accessToken && !!token && !!userRole && !!userID, }); @@ -116,21 +136,12 @@ export default function SpendLogsTable({ 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; + + // No need for additional filtering since we're now handling this in the API call + return matchesSearch; }) || []; + return (

Traces

@@ -160,7 +171,7 @@ export default function SpendLogsTable({ />
-
+
+ {showColumnDropdown && ( +
+ {["Team ID", "Key Hash"].map((option) => ( + + ))} +
+ )} +
+ { + if (selectedFilter === "Team ID") { + setTempTeamId(e.target.value); + } else { + setTempKeyHash(e.target.value); + } + }} + /> + +
+ +
+ + - {showColumnDropdown && ( -
- {["Key Name", "Team Name"].map((option) => ( - - ))} -
- )}
- { - if (selectedColumn === "Key Name") { - setKeyNameFilter(e.target.value); - } else { - setTeamNameFilter(e.target.value); - } - }} - /> - )} -
+