diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index de60a0f35bd..dcdc17ef318 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1771,7 +1771,7 @@ async def ui_view_spend_logs( # noqa: PLR0915 if error_code is not None: metadata_filters.append({ "path": ["error_information", "error_code"], - "equals": error_code, + "equals": f'"{error_code}"', }) if metadata_filters: @@ -1781,7 +1781,6 @@ async def ui_view_spend_logs( # noqa: PLR0915 where_conditions["AND"] = where_conditions.get("AND", []) + [ {"metadata": filter_cond} for filter_cond in metadata_filters ] - if end_user is not None: where_conditions["end_user"] = end_user diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py index 99c3fa034d6..5e3652c6d9d 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_management_endpoints.py @@ -1895,9 +1895,12 @@ async def test_ui_view_spend_logs_with_error_code(client): metadata_filter = where_conditions["metadata"] if metadata_filter.get("path") == ["error_information", "error_code"]: error_code = metadata_filter.get("equals") - if error_code == "404": + # Handle both string and integer error codes + # The endpoint wraps error_code in quotes, so strip them for comparison + error_code_value = str(error_code).strip('"') + if error_code_value == "404": return [mock_spend_logs[0]] - elif error_code == "500": + elif error_code_value == "500": return [mock_spend_logs[1]] return mock_spend_logs @@ -1907,9 +1910,12 @@ async def test_ui_view_spend_logs_with_error_code(client): metadata_filter = where_conditions["metadata"] if metadata_filter.get("path") == ["error_information", "error_code"]: error_code = metadata_filter.get("equals") - if error_code == "404": + # Handle both string and integer error codes + # The endpoint wraps error_code in quotes, so strip them for comparison + error_code_value = str(error_code).strip('"') + if error_code_value == "404": return 1 - elif error_code == "500": + elif error_code_value == "500": return 1 return len(mock_spend_logs) @@ -1995,7 +2001,10 @@ async def test_ui_view_spend_logs_with_error_code_and_key_alias(client): elif metadata_filter.get("path") == ["error_information", "error_code"]: error_code_filter = metadata_filter.get("equals") - if key_alias_filter == "test-key-1" and error_code_filter == "500": + # Handle both string and integer error codes + # The endpoint wraps error_code in quotes, so strip them for comparison + error_code_value = str(error_code_filter).strip('"') + if key_alias_filter == "test-key-1" and error_code_value == "500": return [mock_spend_logs[2]] # Only log3 matches both conditions return mock_spend_logs @@ -2012,7 +2021,10 @@ async def test_ui_view_spend_logs_with_error_code_and_key_alias(client): elif metadata_filter.get("path") == ["error_information", "error_code"]: error_code_filter = metadata_filter.get("equals") - if key_alias_filter == "test-key-1" and error_code_filter == "500": + # Handle both string and integer error codes + # The endpoint wraps error_code in quotes, so strip them for comparison + error_code_value = str(error_code_filter).strip('"') + if key_alias_filter == "test-key-1" and error_code_value == "500": return 1 return len(mock_spend_logs) diff --git a/ui/litellm-dashboard/src/components/molecules/filter.tsx b/ui/litellm-dashboard/src/components/molecules/filter.tsx index d581fe9d2ec..d7c40ae0399 100644 --- a/ui/litellm-dashboard/src/components/molecules/filter.tsx +++ b/ui/litellm-dashboard/src/components/molecules/filter.tsx @@ -129,6 +129,7 @@ const FilterComponent: React.FC = ({ "Key Alias", "User ID", "End User", + "Error Code", "Key Hash", "Model", ]; diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index f0464c61f88..21f18168144 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2687,6 +2687,7 @@ export const uiSpendLogsCall = async ( status_filter?: string, model?: string, keyAlias?: string, + error_code?: string, ) => { try { // Construct base URL @@ -2706,6 +2707,7 @@ export const uiSpendLogsCall = async ( if (status_filter) queryParams.append("status_filter", status_filter); if (model) queryParams.append("model", model); if (keyAlias) queryParams.append("key_alias", keyAlias); + if (error_code) queryParams.append("error_code", error_code); // Append query parameters to URL if any exist const queryString = queryParams.toString(); if (queryString) { diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index f6a969a9eed..2cd113bfb61 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -366,6 +366,24 @@ export default function SpendLogsTable({ setExpandedRequestId(requestId); }; + // Function to extract unique error codes from logs + const extractErrorCodes = (logs: LogEntry[], searchText: string = "") => { + const errorCodes = new Set(); + logs.forEach((log) => { + const metadata = log.metadata || {}; + if (metadata.status === "failure" && metadata.error_information) { + const errorCode = metadata.error_information.error_code; + if (errorCode && (!searchText || errorCode.toLowerCase().includes(searchText.toLowerCase()))) { + errorCodes.add(errorCode); + } + } + }); + return Array.from(errorCodes).map((code) => ({ + label: code, + value: code, + })); + }; + const logFilterOptions: FilterOption[] = [ { name: "Team ID", @@ -426,6 +444,14 @@ export default function SpendLogsTable({ return filtered.map((u: string) => ({ label: u, value: u })); }, }, + { + name: "Error Code", + label: "Error Code", + isSearchable: true, + searchFn: async (searchText: string) => { + return extractErrorCodes(logsData.data, searchText); + }, + }, { name: "Key Hash", label: "Key Hash", 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 c46908d44d8..f56e72df496 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 @@ -17,6 +17,7 @@ export const FILTER_KEYS = { END_USER: "End User", STATUS: "Status", KEY_ALIAS: "Key Alias", + ERROR_CODE: "Error Code", } as const; export type FilterKey = keyof typeof FILTER_KEYS; @@ -53,6 +54,7 @@ export function useLogFilterLogic({ [FILTER_KEYS.END_USER]: "", [FILTER_KEYS.STATUS]: "", [FILTER_KEYS.KEY_ALIAS]: "", + [FILTER_KEYS.ERROR_CODE]: "", }), [], ); @@ -94,6 +96,7 @@ export function useLogFilterLogic({ filters[FILTER_KEYS.STATUS] || undefined, filters[FILTER_KEYS.MODEL] || undefined, filters[FILTER_KEYS.KEY_ALIAS] || undefined, + filters[FILTER_KEYS.ERROR_CODE] || undefined, ); if (currentTimestamp === lastSearchTimestamp.current && response.data) { @@ -133,7 +136,8 @@ export function useLogFilterLogic({ filters[FILTER_KEYS.KEY_HASH] || filters[FILTER_KEYS.REQUEST_ID] || filters[FILTER_KEYS.USER_ID] || - filters[FILTER_KEYS.END_USER] + filters[FILTER_KEYS.END_USER] || + filters[FILTER_KEYS.ERROR_CODE] ), [filters], ); @@ -182,6 +186,14 @@ export function useLogFilterLogic({ filteredData = filteredData.filter((log) => log.end_user === filters[FILTER_KEYS.END_USER]); } + if (filters[FILTER_KEYS.ERROR_CODE]) { + filteredData = filteredData.filter((log) => { + const metadata = log.metadata || {}; + const errorInfo = metadata.error_information; + return errorInfo && errorInfo.error_code === filters[FILTER_KEYS.ERROR_CODE]; + }); + } + return { data: filteredData, total: logs.total,