diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 6e49e4244e7..76eaf273628 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -1856,7 +1856,8 @@ async def ui_view_spend_logs( # noqa: PLR0915 raise handle_exception_on_proxy(e) -@lru_cache(maxsize=128) +# NOTE: @lru_cache was removed here because it does not work correctly with async functions. +# It caches the coroutine object, not the actual result, which can cause errors and memory issues. @router.get( "/spend/logs/ui/{request_id}", tags=["Budget & Spend Tracking"], diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx index 54946eb0964..e5c5deaf380 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -1,7 +1,9 @@ import { useState } from "react"; -import { Drawer, Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space } from "antd"; +import { useQuery } from "@tanstack/react-query"; +import { Drawer, Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space, Spin } from "antd"; import moment from "moment"; import { LogEntry } from "../columns"; +import { uiSpendLogDetailsCall } from "../../networking"; import { formatNumberWithCommas } from "@/utils/dataUtils"; import GuardrailViewer from "../GuardrailViewer/GuardrailViewer"; import { CostBreakdownViewer } from "../CostBreakdownViewer"; @@ -45,6 +47,8 @@ export interface LogDetailsDrawerProps { onOpenSettings?: () => void; allLogs?: LogEntry[]; onSelectLog?: (log: LogEntry) => void; + accessToken?: string; + startTime?: string; } /** @@ -64,6 +68,8 @@ export function LogDetailsDrawer({ onOpenSettings, allLogs = [], onSelectLog, + accessToken, + startTime, }: LogDetailsDrawerProps) { const [activeTab, setActiveTab] = useState(TAB_REQUEST); @@ -76,16 +82,35 @@ export function LogDetailsDrawer({ onSelectLog, }); + // Lazy-load log details (messages/response) only when drawer is open + // This fetches data for a single log on-demand instead of prefetching all 50 + const logDetails = useQuery({ + queryKey: ["logDetails", logEntry?.request_id, startTime], + queryFn: async () => { + if (!accessToken || !logEntry?.request_id || !startTime) return null; + return await uiSpendLogDetailsCall(accessToken, logEntry.request_id, startTime); + }, + enabled: open && !!accessToken && !!logEntry?.request_id && !!startTime, + staleTime: 10 * 60 * 1000, // 10 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + }); + if (!logEntry) return null; + // Merge lazy-loaded details into the log entry + const detailsData = logDetails.data as any; + const effectiveMessages = detailsData?.messages || logEntry.messages; + const effectiveResponse = detailsData?.response || logEntry.response; + const isLoadingDetails = logDetails.isLoading; + const metadata = logEntry.metadata || {}; const hasError = metadata.status === "failure"; const errorInfo = hasError ? metadata.error_information : null; - // Check if request/response data is present - const hasMessages = checkHasMessages(logEntry.messages); - const hasResponse = checkHasResponse(logEntry.response); - const missingData = !hasMessages && !hasResponse; + // Check if request/response data is present (using lazy-loaded data) + const hasMessages = checkHasMessages(effectiveMessages); + const hasResponse = checkHasResponse(effectiveResponse); + const missingData = !hasMessages && !hasResponse && !isLoadingDetails; // Guardrail data const guardrailInfo = metadata?.guardrail_information; @@ -103,7 +128,7 @@ export function LogDetailsDrawer({ const environment = metadata?.user_api_key_team_alias || "default"; const getRawRequest = () => { - return formatData(logEntry.proxy_server_request || logEntry.messages); + return formatData(logEntry.proxy_server_request || effectiveMessages); }; const getFormattedResponse = () => { @@ -117,7 +142,7 @@ export function LogDetailsDrawer({ }, }; } - return formatData(logEntry.response); + return formatData(effectiveResponse); }; return ( @@ -204,12 +229,19 @@ export function LogDetailsDrawer({ )} {/* Request/Response JSON - Collapsible */} - + {isLoadingDetails ? ( +
+ +
Loading request & response data...
+
+ ) : ( + + )} {/* Guardrail Data - Show only if present */} {hasGuardrailData && } diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 3859a5e51fb..cc40981fa3c 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -24,7 +24,6 @@ import { CostBreakdownViewer } from "./CostBreakdownViewer"; import { ErrorViewer } from "./ErrorViewer"; import { useLogFilterLogic } from "./log_filter_logic"; import { getTimeRangeDisplay } from "./logs_utils"; -import { prefetchLogDetails } from "./prefetch"; import { RequestResponsePanel } from "./RequestResponsePanel"; import { SessionView } from "./SessionView"; import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsModal"; @@ -192,6 +191,8 @@ export default function SpendLogsTable({ : moment().utc().format("YYYY-MM-DD HH:mm:ss"); // Get base response from API + // NOTE: We only fetch the list of logs here (lightweight). + // Log details (messages/response) are fetched on-demand when user clicks a row. const response = await uiSpendLogsCall( accessToken, selectedKeyHash || undefined, @@ -207,25 +208,6 @@ export default function SpendLogsTable({ selectedModel, ); - // Trigger prefetch for all logs - await prefetchLogDetails(response.data, formattedStartTime, accessToken, queryClient); - - // Update logs with prefetched data if available - response.data = response.data.map((log: LogEntry) => { - const prefetchedData = queryClient.getQueryData([ - "logDetails", - log.request_id, - formattedStartTime, - ]); - - if (prefetchedData?.messages && prefetchedData?.response) { - log.messages = prefetchedData.messages; - log.response = prefetchedData.response; - return log; - } - return log; - }); - return response; }, enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", @@ -782,6 +764,8 @@ export default function SpendLogsTable({ onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)} allLogs={filteredData} onSelectLog={handleSelectLog} + accessToken={accessToken} + startTime={moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss")} /> );