From a11b043f337acc48de3521302393df9aa1545b10 Mon Sep 17 00:00:00 2001 From: Alexsander Hamir Date: Fri, 30 Jan 2026 14:01:45 -0800 Subject: [PATCH 01/11] fix(proxy): resolve high CPU when router_settings in DB by avoiding REGISTRY.collect() in PrometheusServicesLogger (#20087) --- litellm/integrations/prometheus_services.py | 5 ++ .../integrations/test_prometheus_services.py | 58 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/litellm/integrations/prometheus_services.py b/litellm/integrations/prometheus_services.py index a5f2f0b5c72..55ce758ece6 100644 --- a/litellm/integrations/prometheus_services.py +++ b/litellm/integrations/prometheus_services.py @@ -105,6 +105,11 @@ class PrometheusServicesLogger: return metrics def is_metric_registered(self, metric_name) -> bool: + # Use _names_to_collectors (O(1)) instead of REGISTRY.collect() (O(n)) to avoid + # perf regression when a new Router is created per request (e.g. router_settings in DB). + names_to_collectors = getattr(self.REGISTRY, "_names_to_collectors", None) + if names_to_collectors is not None: + return metric_name in names_to_collectors for metric in self.REGISTRY.collect(): if metric_name == metric.name: return True diff --git a/tests/test_litellm/integrations/test_prometheus_services.py b/tests/test_litellm/integrations/test_prometheus_services.py index b627d31fda0..ff80d7d9f8b 100644 --- a/tests/test_litellm/integrations/test_prometheus_services.py +++ b/tests/test_litellm/integrations/test_prometheus_services.py @@ -1,6 +1,7 @@ import json import os import sys +import time from unittest.mock import AsyncMock, patch import pytest @@ -17,6 +18,63 @@ sys.path.insert( ) # Adds the parent directory to the system path +def test_is_metric_registered_does_not_use_registry_collect(): + """is_metric_registered() must use _names_to_collectors, not REGISTRY.collect() (perf; #19921).""" + from prometheus_client import CollectorRegistry, Counter, Histogram + + registry = CollectorRegistry() + for i in range(80): + Counter( + f"litellm_service_{i}_total_requests", + "Total requests", + labelnames=["service"], + registry=registry, + ) + Histogram( + f"litellm_service_{i}_latency", + "Latency", + labelnames=["service"], + registry=registry, + ) + + pl = PrometheusServicesLogger() + pl.REGISTRY = registry + + original_collect = registry.collect + collect_called = [] + + def track_collect(*args, **kwargs): + collect_called.append(1) + return original_collect(*args, **kwargs) + + registry.collect = track_collect + + n_calls = 30 * 2 + start = time.perf_counter() + for _ in range(30): + pl.is_metric_registered("litellm_service_0_latency") + pl.is_metric_registered("litellm_service_79_total_requests") + elapsed_s = time.perf_counter() - start + elapsed_ms = elapsed_s * 1000 + per_call_us = (elapsed_s / n_calls) * 1_000_000 if n_calls else 0 + n_collect = len(collect_called) + + path = "slow (REGISTRY.collect)" if n_collect else "fast (_names_to_collectors)" + print( + f"\n is_metric_registered: {elapsed_ms:.2f} ms total | " + f"{per_call_us:.1f} µs/call | {n_calls} calls | {n_collect} collect() | {path}\n" + ) + + assert n_collect == 0, ( + f"is_metric_registered() must not use REGISTRY.collect() when _names_to_collectors " + f"is available. Latency: {elapsed_ms:.2f} ms, {per_call_us:.1f} µs/call, {n_calls} calls, " + f"collect() called {n_collect} times." + ) + assert elapsed_s < 0.05, ( + f"is_metric_registered() took {elapsed_ms:.2f} ms for {n_calls} calls; expected <50 ms." + ) + + def test_create_gauge_new(): """Test creating a new gauge""" pl = PrometheusServicesLogger() From 8b7a9250ceb92f9dba3b442a08ce799f32c21cce Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 15:18:00 -0800 Subject: [PATCH 02/11] v0 - looks decen view --- .../components/view_logs/LogDetailsDrawer.tsx | 422 ++++++++++++++++++ .../src/components/view_logs/columns.tsx | 40 -- .../src/components/view_logs/index.tsx | 40 +- .../src/components/view_logs/table.tsx | 24 +- 4 files changed, 460 insertions(+), 66 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx new file mode 100644 index 00000000000..8259bc1e003 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx @@ -0,0 +1,422 @@ +import { Drawer, Typography, Button, Descriptions, Card, Tag, Tooltip, Tabs, message } from "antd"; +import { CloseOutlined, CopyOutlined } from "@ant-design/icons"; +import { Row } from "@tanstack/react-table"; +import { LogEntry } from "./columns"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { truncateString } from "@/utils/textUtils"; +import GuardrailViewer from "./GuardrailViewer/GuardrailViewer"; +import { CostBreakdownViewer } from "./CostBreakdownViewer"; +import { ConfigInfoMessage } from "./ConfigInfoMessage"; +import { RequestResponsePanel } from "./RequestResponsePanel"; +import { VectorStoreViewer } from "./VectorStoreViewer"; +import { ErrorViewer } from "./ErrorViewer"; +import { JsonView, defaultStyles } from "react-json-view-lite"; +import "react-json-view-lite/dist/index.css"; + +const { Title, Text } = Typography; + +interface LogDetailsDrawerProps { + open: boolean; + onClose: () => void; + logEntry: LogEntry | null; + onOpenSettings?: () => void; +} + +export function LogDetailsDrawer({ open, onClose, logEntry, onOpenSettings }: LogDetailsDrawerProps) { + if (!logEntry) return null; + + // Helper function to clean metadata by removing specific fields + const formatData = (input: any) => { + if (typeof input === "string") { + try { + return JSON.parse(input); + } catch { + return input; + } + } + return input; + }; + + // Helper function to get raw request + const getRawRequest = () => { + // First check if proxy_server_request exists in metadata + if (logEntry?.proxy_server_request) { + return formatData(logEntry.proxy_server_request); + } + // Fall back to messages if proxy_server_request is empty + return formatData(logEntry.messages); + }; + + // Extract error information from metadata if available + const metadata = logEntry.metadata || {}; + const hasError = metadata.status === "failure"; + const errorInfo = hasError ? metadata.error_information : null; + + // Check if request/response data is missing + const hasMessages = + logEntry.messages && + (Array.isArray(logEntry.messages) + ? logEntry.messages.length > 0 + : Object.keys(logEntry.messages).length > 0); + const hasResponse = logEntry.response && Object.keys(formatData(logEntry.response)).length > 0; + const missingData = !hasMessages && !hasResponse; + + // Format the response with error details if present + const formattedResponse = () => { + if (hasError && errorInfo) { + return { + error: { + message: errorInfo.error_message || "An error occurred", + type: errorInfo.error_class || "error", + code: errorInfo.error_code || "unknown", + param: null, + }, + }; + } + return formatData(logEntry.response); + }; + + // Extract vector store request metadata if available + const hasVectorStoreData = + metadata.vector_store_request_metadata && + Array.isArray(metadata.vector_store_request_metadata) && + metadata.vector_store_request_metadata.length > 0; + + // Extract guardrail information from metadata if available + const guardrailInfo = logEntry.metadata?.guardrail_information; + const guardrailEntries = Array.isArray(guardrailInfo) ? guardrailInfo : guardrailInfo ? [guardrailInfo] : []; + const hasGuardrailData = guardrailEntries.length > 0; + + // Calculate total masked entities if guardrail data exists + const totalMaskedEntities = guardrailEntries.reduce((sum, entry) => { + const maskedCounts = entry?.masked_entity_count; + if (!maskedCounts) { + return sum; + } + return ( + sum + + Object.values(maskedCounts).reduce((acc, count) => (typeof count === "number" ? acc + count : acc), 0) + ); + }, 0); + + const primaryGuardrailLabel = + guardrailEntries.length === 1 + ? guardrailEntries[0]?.guardrail_name ?? "-" + : guardrailEntries.length > 1 + ? `${guardrailEntries.length} guardrails` + : "-"; + + const handleCopyRequestId = () => { + navigator.clipboard.writeText(logEntry.request_id); + message.success("Request ID copied to clipboard"); + }; + + const copyToClipboard = async (text: string, label: string) => { + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + message.success(`${label} copied to clipboard`); + return true; + } else { + // Fallback for non-secure contexts + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.opacity = "0"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + const successful = document.execCommand("copy"); + document.body.removeChild(textArea); + + if (!successful) { + throw new Error("execCommand failed"); + } + message.success(`${label} copied to clipboard`); + return true; + } + } catch (error) { + console.error("Copy failed:", error); + message.error(`Failed to copy ${label}`); + return false; + } + }; + + return ( + + {/* Custom Header with Request ID prominently displayed */} +
+ {/* Request ID at top - like Langfuse trace ID */} +
+
+ + + {logEntry.request_id} + + + +
+
+ + {/* Status and timestamp row */} +
+ + {metadata.status === "failure" ? "Failure" : "Success"} + + + {logEntry.startTime} + +
+
+ + {/* Scrollable content area */} +
+ {/* Request Details Section */} + + + {logEntry.model} + {logEntry.custom_llm_provider || "-"} + {logEntry.call_type} + {logEntry.model_id} + + + + {logEntry.api_base || "-"} + + + + {logEntry.requester_ip_address && ( + {logEntry.requester_ip_address} + )} + {hasGuardrailData && ( + + {primaryGuardrailLabel} + {totalMaskedEntities > 0 && ( + + {totalMaskedEntities} masked + + )} + + )} + + + + {/* Metrics Section */} + + + + {logEntry.total_tokens} ({logEntry.prompt_tokens} prompt + {logEntry.completion_tokens} completion) + + ${formatNumberWithCommas(logEntry.spend || 0, 6)} + {logEntry.duration} s + {logEntry.cache_hit} + + {formatNumberWithCommas(metadata?.additional_usage_values?.cache_read_input_tokens || 0)} + + + {formatNumberWithCommas(metadata?.additional_usage_values?.cache_creation_input_tokens || 0)} + + {logEntry.startTime} + {logEntry.endTime} + {metadata?.litellm_overhead_time_ms !== undefined && ( + {metadata.litellm_overhead_time_ms} ms + )} + + + + {/* Cost Breakdown - Show if cost breakdown data is available */} + + + {/* Configuration Info Message - Show when data is missing */} + + + {/* Request/Response JSON - Using Tabs */} + + + +
+
+ +
+
+
+ ), + }, + { + key: "response", + label: "Response", + children: ( +
+ +
+ {hasResponse ? ( +
+ +
+ ) : ( +
+ Response data not available +
+ )} +
+
+ ), + }, + ]} + /> + + + {/* Guardrail Data - Show only if present */} + {hasGuardrailData && ( +
+ +
+ )} + + {/* Vector Store Request Data - Show only if present */} + {hasVectorStoreData && ( +
+ +
+ )} + + {/* Error Card - Only show for failures */} + {hasError && errorInfo && ( +
+ +
+ )} + + {/* Tags Card - Only show if there are tags */} + {logEntry.request_tags && Object.keys(logEntry.request_tags).length > 0 && ( + +
+ {Object.entries(logEntry.request_tags).map(([key, value]) => ( + + {key}: {String(value)} + + ))} +
+
+ )} + + {/* Metadata Card - Only show if there's metadata */} + {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( + } + onClick={() => copyToClipboard(JSON.stringify(logEntry.metadata, null, 2), "Metadata")} + > + Copy + + } + > +
+              {JSON.stringify(logEntry.metadata, null, 2)}
+            
+
+ )} + +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/columns.tsx b/ui/litellm-dashboard/src/components/view_logs/columns.tsx index 2da1e83747b..3e72c8e13b8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/columns.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/columns.tsx @@ -49,46 +49,6 @@ export type LogEntry = { }; export const columns: ColumnDef[] = [ - { - id: "expander", - header: () => null, - cell: ({ row }) => { - // Convert the cell function to a React component to properly use hooks - const ExpanderCell = () => { - const [localExpanded, setLocalExpanded] = React.useState(row.getIsExpanded()); - - // Memoize the toggle handler to prevent unnecessary re-renders - const toggleHandler = React.useCallback(() => { - setLocalExpanded((prev) => !prev); - row.getToggleExpandedHandler()(); - }, [row]); - - return row.getCanExpand() ? ( - - ) : ( - - ); - }; - - // Return the component - return ; - }, - }, { header: "Time", accessorKey: "startTime", diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 826fc7ccc02..05188801243 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -31,6 +31,7 @@ import SpendLogsSettingsModal from "./SpendLogsSettingsModal/SpendLogsSettingsMo import { DataTable } from "./table"; import { VectorStoreViewer } from "./VectorStoreViewer"; import NewBadge from "../common_components/NewBadge"; +import { LogDetailsDrawer } from "./LogDetailsDrawer"; interface SpendLogsTableProps { accessToken: string | null; @@ -89,7 +90,8 @@ export default function SpendLogsTable({ const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole)); const [activeTab, setActiveTab] = useState("request logs"); - const [expandedRequestId, setExpandedRequestId] = useState(null); + const [selectedLog, setSelectedLog] = useState(null); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [selectedSessionId, setSelectedSessionId] = useState(null); const [isSpendLogsSettingsModalVisible, setIsSpendLogsSettingsModalVisible] = useState(false); @@ -317,17 +319,6 @@ export default function SpendLogsTable({ enabled: !!accessToken && !!selectedSessionId, }); - // Add this effect to preserve expanded state when data refreshes - useEffect(() => { - if (logs.data?.data && expandedRequestId) { - // Check if the expanded request ID still exists in the new data - const stillExists = logs.data.data.some((log) => log.request_id === expandedRequestId); - if (!stillExists) { - // If the request ID no longer exists in the data, clear the expanded state - setExpandedRequestId(null); - } - } - }, [logs.data?.data, expandedRequestId]); if (!accessToken || !token || !userRole || !userID) { return null; @@ -367,8 +358,14 @@ export default function SpendLogsTable({ logs.refetch(); }; - const handleRowExpand = (requestId: string | null) => { - setExpandedRequestId(requestId); + const handleRowClick = (log: LogEntry) => { + setSelectedLog(log); + setIsDrawerOpen(true); + }; + + const handleCloseDrawer = () => { + setIsDrawerOpen(false); + // Optionally keep selectedLog for animation purposes }; // Function to extract unique error codes from logs @@ -554,9 +551,7 @@ export default function SpendLogsTable({ setIsSpendLogsSettingsModalVisible(true)} />} - getRowCanExpand={() => true} - // Optionally: add session-specific row expansion state + onRowClick={handleRowClick} /> ) : ( @@ -753,8 +748,7 @@ export default function SpendLogsTable({ setIsSpendLogsSettingsModalVisible(true)} />} - getRowCanExpand={() => true} + onRowClick={handleRowClick} /> @@ -775,6 +769,14 @@ export default function SpendLogsTable({ + + {/* Log Details Drawer */} + setIsSpendLogsSettingsModalVisible(true)} + /> ); } diff --git a/ui/litellm-dashboard/src/components/view_logs/table.tsx b/ui/litellm-dashboard/src/components/view_logs/table.tsx index 605341cb2ed..fb7706cba19 100644 --- a/ui/litellm-dashboard/src/components/view_logs/table.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/table.tsx @@ -6,8 +6,10 @@ import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell } fro interface DataTableProps { data: TData[]; columns: ColumnDef[]; - renderSubComponent: (props: { row: Row }) => React.ReactElement; - getRowCanExpand: (row: Row) => boolean; + onRowClick?: (row: TData) => void; + // Legacy props for backward compatibility (audit logs) + renderSubComponent?: (props: { row: Row }) => React.ReactElement; + getRowCanExpand?: (row: Row) => boolean; isLoading?: boolean; loadingMessage?: string; noDataMessage?: string; @@ -16,22 +18,26 @@ interface DataTableProps { export function DataTable({ data = [], columns, - getRowCanExpand, + onRowClick, renderSubComponent, + getRowCanExpand, isLoading = false, loadingMessage = "🚅 Loading logs...", noDataMessage = "No logs found", }: DataTableProps) { + // Determine if we're in legacy expansion mode or new drawer mode + const isLegacyMode = !!renderSubComponent && !!getRowCanExpand; + const table = useReactTable({ data, columns, - getRowCanExpand, + ...(isLegacyMode && { getRowCanExpand }), getRowId: (row: TData, index: number) => { const _row: any = row as any; return _row?.request_id ?? String(index); }, getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), + ...(isLegacyMode && { getExpandedRowModel: getExpandedRowModel() }), }); return ( @@ -62,7 +68,10 @@ export function DataTable({ ) : table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map((row) => ( - + !isLegacyMode && onRowClick?.(row.original)} + > {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -70,7 +79,8 @@ export function DataTable({ ))} - {row.getIsExpanded() && ( + {/* Legacy expansion mode for audit logs */} + {isLegacyMode && row.getIsExpanded() && renderSubComponent && (
{renderSubComponent({ row })}
From f07ef8af00a4010145c415ef39a673d96d5e00c8 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 15:32:10 -0800 Subject: [PATCH 03/11] refactored code --- .../components/view_logs/LogDetailsDrawer.tsx | 422 -------------- .../LogDetailsDrawer/DrawerHeader.tsx | 146 +++++ .../view_logs/LogDetailsDrawer/JsonViewer.tsx | 66 +++ .../LogDetailsDrawer/LogDetailsDrawer.tsx | 516 ++++++++++++++++++ .../view_logs/LogDetailsDrawer/TokenFlow.tsx | 27 + .../LogDetailsDrawer/TruncatedValue.tsx | 35 ++ .../LogDetailsDrawer/clipboardUtils.ts | 43 ++ .../view_logs/LogDetailsDrawer/constants.ts | 48 ++ .../view_logs/LogDetailsDrawer/index.ts | 2 + .../LogDetailsDrawer/useKeyboardNavigation.ts | 87 +++ .../src/components/view_logs/index.tsx | 6 + 11 files changed, 976 insertions(+), 422 deletions(-) delete mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx deleted file mode 100644 index 8259bc1e003..00000000000 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import { Drawer, Typography, Button, Descriptions, Card, Tag, Tooltip, Tabs, message } from "antd"; -import { CloseOutlined, CopyOutlined } from "@ant-design/icons"; -import { Row } from "@tanstack/react-table"; -import { LogEntry } from "./columns"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; -import { truncateString } from "@/utils/textUtils"; -import GuardrailViewer from "./GuardrailViewer/GuardrailViewer"; -import { CostBreakdownViewer } from "./CostBreakdownViewer"; -import { ConfigInfoMessage } from "./ConfigInfoMessage"; -import { RequestResponsePanel } from "./RequestResponsePanel"; -import { VectorStoreViewer } from "./VectorStoreViewer"; -import { ErrorViewer } from "./ErrorViewer"; -import { JsonView, defaultStyles } from "react-json-view-lite"; -import "react-json-view-lite/dist/index.css"; - -const { Title, Text } = Typography; - -interface LogDetailsDrawerProps { - open: boolean; - onClose: () => void; - logEntry: LogEntry | null; - onOpenSettings?: () => void; -} - -export function LogDetailsDrawer({ open, onClose, logEntry, onOpenSettings }: LogDetailsDrawerProps) { - if (!logEntry) return null; - - // Helper function to clean metadata by removing specific fields - const formatData = (input: any) => { - if (typeof input === "string") { - try { - return JSON.parse(input); - } catch { - return input; - } - } - return input; - }; - - // Helper function to get raw request - const getRawRequest = () => { - // First check if proxy_server_request exists in metadata - if (logEntry?.proxy_server_request) { - return formatData(logEntry.proxy_server_request); - } - // Fall back to messages if proxy_server_request is empty - return formatData(logEntry.messages); - }; - - // Extract error information from metadata if available - const metadata = logEntry.metadata || {}; - const hasError = metadata.status === "failure"; - const errorInfo = hasError ? metadata.error_information : null; - - // Check if request/response data is missing - const hasMessages = - logEntry.messages && - (Array.isArray(logEntry.messages) - ? logEntry.messages.length > 0 - : Object.keys(logEntry.messages).length > 0); - const hasResponse = logEntry.response && Object.keys(formatData(logEntry.response)).length > 0; - const missingData = !hasMessages && !hasResponse; - - // Format the response with error details if present - const formattedResponse = () => { - if (hasError && errorInfo) { - return { - error: { - message: errorInfo.error_message || "An error occurred", - type: errorInfo.error_class || "error", - code: errorInfo.error_code || "unknown", - param: null, - }, - }; - } - return formatData(logEntry.response); - }; - - // Extract vector store request metadata if available - const hasVectorStoreData = - metadata.vector_store_request_metadata && - Array.isArray(metadata.vector_store_request_metadata) && - metadata.vector_store_request_metadata.length > 0; - - // Extract guardrail information from metadata if available - const guardrailInfo = logEntry.metadata?.guardrail_information; - const guardrailEntries = Array.isArray(guardrailInfo) ? guardrailInfo : guardrailInfo ? [guardrailInfo] : []; - const hasGuardrailData = guardrailEntries.length > 0; - - // Calculate total masked entities if guardrail data exists - const totalMaskedEntities = guardrailEntries.reduce((sum, entry) => { - const maskedCounts = entry?.masked_entity_count; - if (!maskedCounts) { - return sum; - } - return ( - sum + - Object.values(maskedCounts).reduce((acc, count) => (typeof count === "number" ? acc + count : acc), 0) - ); - }, 0); - - const primaryGuardrailLabel = - guardrailEntries.length === 1 - ? guardrailEntries[0]?.guardrail_name ?? "-" - : guardrailEntries.length > 1 - ? `${guardrailEntries.length} guardrails` - : "-"; - - const handleCopyRequestId = () => { - navigator.clipboard.writeText(logEntry.request_id); - message.success("Request ID copied to clipboard"); - }; - - const copyToClipboard = async (text: string, label: string) => { - try { - // Try modern clipboard API first - if (navigator.clipboard && window.isSecureContext) { - await navigator.clipboard.writeText(text); - message.success(`${label} copied to clipboard`); - return true; - } else { - // Fallback for non-secure contexts - const textArea = document.createElement("textarea"); - textArea.value = text; - textArea.style.position = "fixed"; - textArea.style.opacity = "0"; - document.body.appendChild(textArea); - textArea.focus(); - textArea.select(); - - const successful = document.execCommand("copy"); - document.body.removeChild(textArea); - - if (!successful) { - throw new Error("execCommand failed"); - } - message.success(`${label} copied to clipboard`); - return true; - } - } catch (error) { - console.error("Copy failed:", error); - message.error(`Failed to copy ${label}`); - return false; - } - }; - - return ( - - {/* Custom Header with Request ID prominently displayed */} -
- {/* Request ID at top - like Langfuse trace ID */} -
-
- - - {logEntry.request_id} - - - -
-
- - {/* Status and timestamp row */} -
- - {metadata.status === "failure" ? "Failure" : "Success"} - - - {logEntry.startTime} - -
-
- - {/* Scrollable content area */} -
- {/* Request Details Section */} - - - {logEntry.model} - {logEntry.custom_llm_provider || "-"} - {logEntry.call_type} - {logEntry.model_id} - - - - {logEntry.api_base || "-"} - - - - {logEntry.requester_ip_address && ( - {logEntry.requester_ip_address} - )} - {hasGuardrailData && ( - - {primaryGuardrailLabel} - {totalMaskedEntities > 0 && ( - - {totalMaskedEntities} masked - - )} - - )} - - - - {/* Metrics Section */} - - - - {logEntry.total_tokens} ({logEntry.prompt_tokens} prompt + {logEntry.completion_tokens} completion) - - ${formatNumberWithCommas(logEntry.spend || 0, 6)} - {logEntry.duration} s - {logEntry.cache_hit} - - {formatNumberWithCommas(metadata?.additional_usage_values?.cache_read_input_tokens || 0)} - - - {formatNumberWithCommas(metadata?.additional_usage_values?.cache_creation_input_tokens || 0)} - - {logEntry.startTime} - {logEntry.endTime} - {metadata?.litellm_overhead_time_ms !== undefined && ( - {metadata.litellm_overhead_time_ms} ms - )} - - - - {/* Cost Breakdown - Show if cost breakdown data is available */} - - - {/* Configuration Info Message - Show when data is missing */} - - - {/* Request/Response JSON - Using Tabs */} - - - -
-
- -
-
-
- ), - }, - { - key: "response", - label: "Response", - children: ( -
- -
- {hasResponse ? ( -
- -
- ) : ( -
- Response data not available -
- )} -
-
- ), - }, - ]} - /> - - - {/* Guardrail Data - Show only if present */} - {hasGuardrailData && ( -
- -
- )} - - {/* Vector Store Request Data - Show only if present */} - {hasVectorStoreData && ( -
- -
- )} - - {/* Error Card - Only show for failures */} - {hasError && errorInfo && ( -
- -
- )} - - {/* Tags Card - Only show if there are tags */} - {logEntry.request_tags && Object.keys(logEntry.request_tags).length > 0 && ( - -
- {Object.entries(logEntry.request_tags).map(([key, value]) => ( - - {key}: {String(value)} - - ))} -
-
- )} - - {/* Metadata Card - Only show if there's metadata */} - {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( - } - onClick={() => copyToClipboard(JSON.stringify(logEntry.metadata, null, 2), "Metadata")} - > - Copy - - } - > -
-              {JSON.stringify(logEntry.metadata, null, 2)}
-            
-
- )} - -
- ); -} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx new file mode 100644 index 00000000000..364959b0b58 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx @@ -0,0 +1,146 @@ +import { Button, Tag, Tooltip, Typography } from "antd"; +import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; +import moment from "moment"; +import { LogEntry } from "../columns"; +import { + DRAWER_HEADER_PADDING, + COLOR_BORDER, + COLOR_BACKGROUND, + SPACING_MEDIUM, + SPACING_LARGE, + FONT_SIZE_HEADER, + FONT_SIZE_MEDIUM, + FONT_FAMILY_MONO, + SPACING_SMALL, +} from "./constants"; + +const { Text } = Typography; + +interface DrawerHeaderProps { + log: LogEntry; + onClose: () => void; + onCopyRequestId: () => void; + onPrevious: () => void; + onNext: () => void; + statusLabel: string; + statusColor: "error" | "success"; + environment: string; +} + +/** + * Header component for the log details drawer. + * Displays request ID, navigation controls, status, environment, and timestamp. + */ +export function DrawerHeader({ + log, + onClose, + onCopyRequestId, + onPrevious, + onNext, + statusLabel, + statusColor, + environment, +}: DrawerHeaderProps) { + return ( +
+ {/* Row 1: Request ID + Actions */} +
+ + +
+ + {/* Row 2: Status + Env + Timestamp */} + +
+ ); +} + +/** + * Request ID display with copy button + */ +function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: () => void }) { + return ( +
+ + + {requestId} + + + +
+ ); +} + +/** + * Navigation controls (previous, next, close) + */ +function NavigationSection({ + onPrevious, + onNext, + onClose, +}: { + onPrevious: () => void; + onNext: () => void; + onClose: () => void; +}) { + return ( +
+ +
+ ); +} + +/** + * Status bar with tags and timestamp + */ +function StatusBar({ + log, + statusLabel, + statusColor, + environment, +}: { + log: LogEntry; + statusLabel: string; + statusColor: "error" | "success"; + environment: string; +}) { + return ( +
+ {statusLabel} + Env: {environment} + + {moment(log.startTime).format("MMM D, YYYY h:mm:ss A")} + ({moment(log.startTime).fromNow()}) + +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx new file mode 100644 index 00000000000..1f1d9359730 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx @@ -0,0 +1,66 @@ +import { Typography } from "antd"; +import { JsonView, defaultStyles } from "react-json-view-lite"; +import "react-json-view-lite/dist/index.css"; +import { + JSON_MAX_HEIGHT, + FONT_SIZE_SMALL, + COLOR_BG_LIGHT, + SPACING_LARGE, + FONT_FAMILY_MONO, + VIEW_MODE_JSON, +} from "./constants"; + +const { Text } = Typography; + +export type ViewMode = "formatted" | "json"; + +interface JsonViewerProps { + data: any; + mode: ViewMode; +} + +/** + * Displays JSON data in either formatted tree view or raw JSON format. + * Formatted view uses an interactive tree, JSON view shows raw stringified output. + */ +export function JsonViewer({ data, mode }: JsonViewerProps) { + if (!data) return No data; + + if (mode === VIEW_MODE_JSON) { + return ( +
+        {JSON.stringify(data, null, 2)}
+      
+ ); + } + + // Formatted tree view + return ( +
+
+ +
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx new file mode 100644 index 00000000000..e1334244182 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -0,0 +1,516 @@ +import { useState } from "react"; +import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Radio, Alert, message } from "antd"; +import { CopyOutlined } from "@ant-design/icons"; +import moment from "moment"; +import { LogEntry } from "../columns"; +import { formatNumberWithCommas } from "@/utils/dataUtils"; +import GuardrailViewer from "../GuardrailViewer/GuardrailViewer"; +import { CostBreakdownViewer } from "../CostBreakdownViewer"; +import { ConfigInfoMessage } from "../ConfigInfoMessage"; +import { VectorStoreViewer } from "../VectorStoreViewer"; +import { TruncatedValue } from "./TruncatedValue"; +import { TokenFlow } from "./TokenFlow"; +import { JsonViewer, ViewMode } from "./JsonViewer"; +import { DrawerHeader } from "./DrawerHeader"; +import { copyToClipboard } from "./clipboardUtils"; +import { useKeyboardNavigation } from "./useKeyboardNavigation"; +import { + DRAWER_WIDTH, + DRAWER_CONTENT_PADDING, + API_BASE_MAX_WIDTH, + METADATA_MAX_HEIGHT, + TAB_REQUEST, + TAB_RESPONSE, + VIEW_MODE_FORMATTED, + FONT_SIZE_SMALL, + FONT_FAMILY_MONO, + SPACING_XLARGE, + MESSAGE_REQUEST_ID_COPIED, +} from "./constants"; + +const { Text } = Typography; + +export interface LogDetailsDrawerProps { + open: boolean; + onClose: () => void; + logEntry: LogEntry | null; + onOpenSettings?: () => void; + allLogs?: LogEntry[]; + onSelectLog?: (log: LogEntry) => void; +} + +/** + * Right-side drawer panel for displaying detailed log information. + * Features: + * - Request ID prominently displayed with copy functionality + * - Keyboard navigation (J/K for next/prev, Escape to close) + * - Formatted and JSON view toggle for request/response + * - Smart display of cache fields (hidden when zero) + * - Error alerts for failed requests + * - Collapsible sections for guardrails, vector store, metadata + */ +export function LogDetailsDrawer({ + open, + onClose, + logEntry, + onOpenSettings, + allLogs = [], + onSelectLog, +}: LogDetailsDrawerProps) { + const [activeTab, setActiveTab] = useState(TAB_REQUEST); + const [jsonViewMode, setJsonViewMode] = useState(VIEW_MODE_FORMATTED); + + // Keyboard navigation + const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({ + isOpen: open, + currentLog: logEntry, + allLogs, + onClose, + onSelectLog, + }); + + if (!logEntry) return null; + + 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; + + // Guardrail data + const guardrailInfo = metadata?.guardrail_information; + const guardrailEntries = normalizeGuardrailEntries(guardrailInfo); + const hasGuardrailData = guardrailEntries.length > 0; + const totalMaskedEntities = calculateTotalMaskedEntities(guardrailEntries); + const primaryGuardrailLabel = getGuardrailLabel(guardrailEntries); + + // Vector store data + const hasVectorStoreData = checkHasVectorStoreData(metadata); + + // Status display values + const statusLabel = metadata.status === "failure" ? "Failure" : "Success"; + const statusColor = metadata.status === "failure" ? ("error" as const) : ("success" as const); + const environment = metadata?.user_api_key_team_alias || "default"; + + const handleCopyRequestId = () => { + navigator.clipboard.writeText(logEntry.request_id); + message.success(MESSAGE_REQUEST_ID_COPIED); + }; + + const getRawRequest = () => { + return formatData(logEntry.proxy_server_request || logEntry.messages); + }; + + const getFormattedResponse = () => { + if (hasError && errorInfo) { + return { + error: { + message: errorInfo.error_message || "An error occurred", + type: errorInfo.error_class || "error", + code: errorInfo.error_code || "unknown", + param: null, + }, + }; + } + return formatData(logEntry.response); + }; + + return ( + + + +
+ {/* Error Alert - Show prominently at top for failures */} + {hasError && errorInfo && ( + } + style={{ marginBottom: SPACING_XLARGE }} + /> + )} + + {/* Tags - Only show if present */} + {logEntry.request_tags && Object.keys(logEntry.request_tags).length > 0 && ( + + )} + + {/* Request Details Section */} + + + {logEntry.model} + {logEntry.custom_llm_provider || "-"} + {logEntry.call_type} + + + + + + + {logEntry.requester_ip_address && ( + {logEntry.requester_ip_address} + )} + {hasGuardrailData && ( + + + + )} + + + + {/* Metrics Section */} + + + {/* Cost Breakdown - Show if cost breakdown data is available */} + + + {/* Configuration Info Message - Show when data is missing */} + + + {/* Request/Response JSON - Using Tabs with View Toggle */} + copyToClipboard(JSON.stringify(data, null, 2), label)} + getRawRequest={getRawRequest} + getFormattedResponse={getFormattedResponse} + /> + + {/* Guardrail Data - Show only if present */} + {hasGuardrailData && ( +
+ +
+ )} + + {/* Vector Store Request Data - Show only if present */} + {hasVectorStoreData && ( +
+ +
+ )} + + {/* Metadata Card - Only show if there's metadata */} + {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( + copyToClipboard(data, "Metadata")} /> + )} +
+
+ ); +} + +// ============================================================================ +// Helper Components +// ============================================================================ + +function ErrorDescription({ errorInfo }: { errorInfo: any }) { + return ( +
+ {errorInfo.error_code && ( +
+ Error Code: {errorInfo.error_code} +
+ )} + {errorInfo.error_message && ( +
+ Message: {errorInfo.error_message} +
+ )} +
+ ); +} + +function TagsSection({ tags }: { tags: Record }) { + return ( +
+ + Tags + +
+ {Object.entries(tags).map(([key, value]) => ( + + {key}: {String(value)} + + ))} +
+
+ ); +} + +function GuardrailLabel({ label, maskedCount }: { label: string; maskedCount: number }) { + return ( + <> + {label} + {maskedCount > 0 && ( + + {maskedCount} masked + + )} + + ); +} + +function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: Record }) { + const hasCacheActivity = + logEntry.cache_hit || + (metadata?.additional_usage_values?.cache_read_input_tokens && + metadata.additional_usage_values.cache_read_input_tokens > 0); + + return ( + + + + + + ${formatNumberWithCommas(logEntry.spend || 0, 8)} + {logEntry.duration?.toFixed(3)} s + + {/* Only show cache fields if there's cache activity */} + {hasCacheActivity && ( + <> + + {logEntry.cache_hit || "None"} + + {metadata?.additional_usage_values?.cache_read_input_tokens > 0 && ( + + {formatNumberWithCommas(metadata.additional_usage_values.cache_read_input_tokens)} + + )} + {metadata?.additional_usage_values?.cache_creation_input_tokens > 0 && ( + + {formatNumberWithCommas(metadata.additional_usage_values.cache_creation_input_tokens)} + + )} + + )} + + {metadata?.litellm_overhead_time_ms !== undefined && ( + + {metadata.litellm_overhead_time_ms.toFixed(2)} ms + + )} + + + {moment(logEntry.startTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")} + + + {moment(logEntry.endTime).format("YYYY-MM-DDTHH:mm:ss.SSS[Z]")} + + + + ); +} + +interface RequestResponseSectionProps { + activeTab: typeof TAB_REQUEST | typeof TAB_RESPONSE; + jsonViewMode: ViewMode; + hasResponse: boolean; + onTabChange: (key: typeof TAB_REQUEST | typeof TAB_RESPONSE) => void; + onViewModeChange: (mode: ViewMode) => void; + onCopy: (data: any, label: string) => void; + getRawRequest: () => any; + getFormattedResponse: () => any; +} + +function RequestResponseSection({ + activeTab, + jsonViewMode, + hasResponse, + onTabChange, + onViewModeChange, + onCopy, + getRawRequest, + getFormattedResponse, +}: RequestResponseSectionProps) { + const handleCopy = () => { + const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); + const label = activeTab === TAB_REQUEST ? "Request" : "Response"; + onCopy(data, label); + }; + + return ( + + onTabChange(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} + tabBarExtraContent={ +
+ {/* View Mode Toggle */} + onViewModeChange(e.target.value)}> + Formatted + JSON + + + {/* Copy Button */} + +
+ } + items={[ + { + key: TAB_REQUEST, + label: "Request", + children: ( +
+ +
+ ), + }, + { + key: TAB_RESPONSE, + label: "Response", + children: ( +
+ {hasResponse ? ( + + ) : ( +
+ Response data not available +
+ )} +
+ ), + }, + ]} + style={{ padding: `0 ${SPACING_XLARGE}px` }} + /> +
+ ); +} + +function MetadataSection({ metadata, onCopy }: { metadata: Record; onCopy: (data: string) => void }) { + return ( + } + onClick={() => onCopy(JSON.stringify(metadata, null, 2))} + > + Copy + + } + > +
+        {JSON.stringify(metadata, null, 2)}
+      
+
+ ); +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +function formatData(input: any) { + if (typeof input === "string") { + try { + return JSON.parse(input); + } catch { + return input; + } + } + return input; +} + +function checkHasMessages(messages: any): boolean { + if (!messages) return false; + if (Array.isArray(messages)) return messages.length > 0; + if (typeof messages === "object") return Object.keys(messages).length > 0; + return false; +} + +function checkHasResponse(response: any): boolean { + if (!response) return false; + return Object.keys(formatData(response)).length > 0; +} + +function normalizeGuardrailEntries(guardrailInfo: any): any[] { + if (Array.isArray(guardrailInfo)) return guardrailInfo; + if (guardrailInfo) return [guardrailInfo]; + return []; +} + +function calculateTotalMaskedEntities(entries: any[]): number { + return entries.reduce((sum, entry) => { + const maskedCounts = entry?.masked_entity_count; + if (!maskedCounts) return sum; + return ( + sum + + Object.values(maskedCounts).reduce((acc, count) => (typeof count === "number" ? acc + count : acc), 0) + ); + }, 0); +} + +function getGuardrailLabel(entries: any[]): string { + if (entries.length === 0) return "-"; + if (entries.length === 1) return entries[0]?.guardrail_name ?? "-"; + return `${entries.length} guardrails`; +} + +function checkHasVectorStoreData(metadata: Record): boolean { + return ( + metadata.vector_store_request_metadata && + Array.isArray(metadata.vector_store_request_metadata) && + metadata.vector_store_request_metadata.length > 0 + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx new file mode 100644 index 00000000000..a9a30e55f17 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx @@ -0,0 +1,27 @@ +import { Typography } from "antd"; +import { COLOR_SECONDARY, FONT_FAMILY_MONO, FONT_SIZE_MEDIUM, SPACING_SMALL, SPACING_MEDIUM } from "./constants"; + +const { Text } = Typography; + +interface TokenFlowProps { + prompt?: number; + completion?: number; + total?: number; +} + +/** + * Displays token usage in a flow format: "prompt → completion (Σ total)" + * Makes it easy to see the relationship between input, output, and total tokens. + */ +export function TokenFlow({ prompt = 0, completion = 0, total = 0 }: TokenFlowProps) { + return ( + + {prompt.toLocaleString()} + + {completion.toLocaleString()} + + (Σ {total.toLocaleString()}) + + + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx new file mode 100644 index 00000000000..b03895808d6 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TruncatedValue.tsx @@ -0,0 +1,35 @@ +import { Typography, Tooltip } from "antd"; +import { DEFAULT_MAX_WIDTH, FONT_FAMILY_MONO, FONT_SIZE_SMALL } from "./constants"; + +const { Text } = Typography; + +interface TruncatedValueProps { + value?: string; + maxWidth?: number; +} + +/** + * Displays a truncated value with tooltip and copy functionality. + * Useful for displaying long IDs, URLs, or other text that may overflow. + */ +export function TruncatedValue({ value, maxWidth = DEFAULT_MAX_WIDTH }: TruncatedValueProps) { + if (!value) return -; + + return ( + + + {value} + + + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts new file mode 100644 index 00000000000..6aae95bb72c --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/clipboardUtils.ts @@ -0,0 +1,43 @@ +import { message } from "antd"; +import { MESSAGE_COPY_SUCCESS } from "./constants"; + +/** + * Copies text to clipboard with fallback for non-secure contexts. + * Shows success/error message to user. + * + * @param text - Text to copy to clipboard + * @param label - Label for the copied content (e.g., "Request", "Metadata") + * @returns Promise - true if copy succeeded, false otherwise + */ +export async function copyToClipboard(text: string, label: string): Promise { + try { + // Try modern clipboard API first + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + message.success(`${label} ${MESSAGE_COPY_SUCCESS}`); + return true; + } else { + // Fallback for non-secure contexts (like 0.0.0.0) + const textArea = document.createElement("textarea"); + textArea.value = text; + textArea.style.position = "fixed"; + textArea.style.opacity = "0"; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + + const successful = document.execCommand("copy"); + document.body.removeChild(textArea); + + if (!successful) { + throw new Error("execCommand failed"); + } + message.success(`${label} ${MESSAGE_COPY_SUCCESS}`); + return true; + } + } catch (error) { + console.error("Copy failed:", error); + message.error(`Failed to copy ${label}`); + return false; + } +} diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts new file mode 100644 index 00000000000..e1222ce00ae --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts @@ -0,0 +1,48 @@ +// Drawer configuration constants +export const DRAWER_WIDTH = "60%"; +export const DRAWER_HEADER_PADDING = "16px 24px"; +export const DRAWER_CONTENT_PADDING = "24px"; + +// Truncation and display limits +export const DEFAULT_MAX_WIDTH = 180; +export const API_BASE_MAX_WIDTH = 200; +export const JSON_MAX_HEIGHT = 400; +export const METADATA_MAX_HEIGHT = 300; + +// Tab keys +export const TAB_REQUEST = "request" as const; +export const TAB_RESPONSE = "response" as const; + +// View modes +export const VIEW_MODE_FORMATTED = "formatted" as const; +export const VIEW_MODE_JSON = "json" as const; + +// Keyboard shortcuts +export const KEY_ESCAPE = "Escape"; +export const KEY_J_LOWER = "j"; +export const KEY_J_UPPER = "J"; +export const KEY_K_LOWER = "k"; +export const KEY_K_UPPER = "K"; + +// Typography +export const FONT_FAMILY_MONO = "monospace"; +export const FONT_SIZE_SMALL = 12; +export const FONT_SIZE_MEDIUM = 13; +export const FONT_SIZE_HEADER = 16; + +// Colors +export const COLOR_BORDER = "#f0f0f0"; +export const COLOR_BACKGROUND = "#fff"; +export const COLOR_SECONDARY = "#8c8c8c"; +export const COLOR_BG_LIGHT = "#fafafa"; + +// Spacing +export const SPACING_SMALL = 4; +export const SPACING_MEDIUM = 8; +export const SPACING_LARGE = 12; +export const SPACING_XLARGE = 16; +export const SPACING_XXLARGE = 24; + +// Messages +export const MESSAGE_COPY_SUCCESS = "copied to clipboard"; +export const MESSAGE_REQUEST_ID_COPIED = "Request ID copied to clipboard"; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts new file mode 100644 index 00000000000..e1fdd9d2d60 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/index.ts @@ -0,0 +1,2 @@ +export { LogDetailsDrawer } from "./LogDetailsDrawer"; +export type { LogDetailsDrawerProps } from "./LogDetailsDrawer"; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts new file mode 100644 index 00000000000..e4fa9bc6185 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/useKeyboardNavigation.ts @@ -0,0 +1,87 @@ +import { useEffect } from "react"; +import { LogEntry } from "../columns"; +import { KEY_ESCAPE, KEY_J_LOWER, KEY_J_UPPER, KEY_K_LOWER, KEY_K_UPPER } from "./constants"; + +interface UseKeyboardNavigationProps { + isOpen: boolean; + currentLog: LogEntry | null; + allLogs: LogEntry[]; + onClose: () => void; + onSelectLog?: (log: LogEntry) => void; +} + +/** + * Custom hook for keyboard navigation in the log details drawer. + * Handles J/K for next/previous and Escape for close. + * + * Keyboard shortcuts: + * - J: Navigate to next log + * - K: Navigate to previous log + * - Escape: Close drawer + */ +export function useKeyboardNavigation({ + isOpen, + currentLog, + allLogs, + onClose, + onSelectLog, +}: UseKeyboardNavigationProps) { + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + // Don't trigger if user is typing in an input + if (isUserTyping(e.target)) { + return; + } + + if (!isOpen) return; + + switch (e.key) { + case KEY_ESCAPE: + onClose(); + break; + case KEY_J_LOWER: + case KEY_J_UPPER: + selectNextLog(); + break; + case KEY_K_LOWER: + case KEY_K_UPPER: + selectPreviousLog(); + break; + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isOpen, currentLog, allLogs]); + + const selectNextLog = () => { + if (!currentLog || !allLogs.length || !onSelectLog) return; + + const currentIndex = allLogs.findIndex((l) => l.request_id === currentLog.request_id); + if (currentIndex < allLogs.length - 1) { + onSelectLog(allLogs[currentIndex + 1]); + } + }; + + const selectPreviousLog = () => { + if (!currentLog || !allLogs.length || !onSelectLog) return; + + const currentIndex = allLogs.findIndex((l) => l.request_id === currentLog.request_id); + if (currentIndex > 0) { + onSelectLog(allLogs[currentIndex - 1]); + } + }; + + return { + selectNextLog, + selectPreviousLog, + }; +} + +/** + * Checks if the user is currently typing in an input field. + * Used to prevent keyboard shortcuts from interfering with text input. + */ +function isUserTyping(target: EventTarget | null): boolean { + return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement; +} diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index 05188801243..3859a5e51fb 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -368,6 +368,10 @@ export default function SpendLogsTable({ // Optionally keep selectedLog for animation purposes }; + const handleSelectLog = (log: LogEntry) => { + setSelectedLog(log); + }; + // Function to extract unique error codes from logs const extractErrorCodes = (logs: LogEntry[], searchText: string = "") => { const errorCodes = new Set(); @@ -776,6 +780,8 @@ export default function SpendLogsTable({ onClose={handleCloseDrawer} logEntry={selectedLog} onOpenSettings={() => setIsSpendLogsSettingsModalVisible(true)} + allLogs={filteredData} + onSelectLog={handleSelectLog} /> ); From 5f076353108f97834f4588dc359a33a13dba0b1a Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 15:42:37 -0800 Subject: [PATCH 04/11] fix ui --- .../LogDetailsDrawer/DrawerHeader.tsx | 77 +++++- .../view_logs/LogDetailsDrawer/JsonViewer.tsx | 41 +--- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 224 +++++++++--------- .../view_logs/LogDetailsDrawer/TokenFlow.tsx | 15 +- .../view_logs/LogDetailsDrawer/constants.ts | 6 +- 5 files changed, 190 insertions(+), 173 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx index 364959b0b58..6b3f7fdaa5b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx @@ -2,6 +2,7 @@ import { Button, Tag, Tooltip, Typography } from "antd"; import { CloseOutlined, CopyOutlined, UpOutlined, DownOutlined } from "@ant-design/icons"; import moment from "moment"; import { LogEntry } from "../columns"; +import { getProviderLogoAndName } from "../../provider_info_helpers"; import { DRAWER_HEADER_PADDING, COLOR_BORDER, @@ -29,7 +30,7 @@ interface DrawerHeaderProps { /** * Header component for the log details drawer. - * Displays request ID, navigation controls, status, environment, and timestamp. + * Displays model/provider, request ID, navigation controls, status, environment, and timestamp. */ export function DrawerHeader({ log, @@ -41,6 +42,9 @@ export function DrawerHeader({ statusColor, environment, }: DrawerHeaderProps) { + const provider = log.custom_llm_provider || ""; + const providerInfo = provider ? getProviderLogoAndName(provider) : null; + return (
+ {/* Row 0: Model + Provider with Logo */} + + {/* Row 1: Request ID + Actions */}
@@ -64,6 +71,45 @@ export function DrawerHeader({ ); } +/** + * Model and Provider display with logo + */ +function ModelProviderSection({ + model, + providerLogo, + providerName, +}: { + model: string; + providerLogo?: string; + providerName?: string; +}) { + return ( +
+ {providerLogo && ( + {providerName { + const target = e.target as HTMLImageElement; + target.style.display = "none"; + }} + /> + )} +
+ + {model} + + {providerName && ( + + {providerName} + + )} +
+
+ ); +} + /** * Request ID display with copy button */ @@ -93,6 +139,7 @@ function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: () /** * Navigation controls (previous, next, close) + * Shows keyboard shortcuts with bounding boxes for visibility */ function NavigationSection({ onPrevious, @@ -103,18 +150,32 @@ function NavigationSection({ onNext: () => void; onClose: () => void; }) { + const keyboardShortcutStyle = { + border: "1px solid #d9d9d9", + borderRadius: 4, + padding: "0 4px", + fontSize: 12, + fontFamily: "monospace", + marginLeft: 4, + background: "#fafafa", + }; + return (
- - +
-
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx index 1f1d9359730..6463abf89d4 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/JsonViewer.tsx @@ -1,53 +1,22 @@ import { Typography } from "antd"; import { JsonView, defaultStyles } from "react-json-view-lite"; import "react-json-view-lite/dist/index.css"; -import { - JSON_MAX_HEIGHT, - FONT_SIZE_SMALL, - COLOR_BG_LIGHT, - SPACING_LARGE, - FONT_FAMILY_MONO, - VIEW_MODE_JSON, -} from "./constants"; +import { JSON_MAX_HEIGHT, COLOR_BG_LIGHT, SPACING_LARGE } from "./constants"; const { Text } = Typography; -export type ViewMode = "formatted" | "json"; - interface JsonViewerProps { data: any; - mode: ViewMode; + mode: "formatted"; } /** - * Displays JSON data in either formatted tree view or raw JSON format. - * Formatted view uses an interactive tree, JSON view shows raw stringified output. + * Displays JSON data in formatted tree view. + * Uses an interactive tree component for easy navigation. */ -export function JsonViewer({ data, mode }: JsonViewerProps) { +export function JsonViewer({ data }: JsonViewerProps) { if (!data) return No data; - if (mode === VIEW_MODE_JSON) { - return ( -
-        {JSON.stringify(data, null, 2)}
-      
- ); - } - - // Formatted tree view return (
(TAB_REQUEST); - const [jsonViewMode, setJsonViewMode] = useState(VIEW_MODE_FORMATTED); // Keyboard navigation const { selectNextLog, selectPreviousLog } = useKeyboardNavigation({ @@ -162,8 +160,9 @@ export function LogDetailsDrawer({ )} {/* Request Details Section */} - - +
+ + {logEntry.model} {logEntry.custom_llm_provider || "-"} {logEntry.call_type} @@ -183,6 +182,7 @@ export function LogDetailsDrawer({ )} +
{/* Metrics Section */} @@ -193,31 +193,19 @@ export function LogDetailsDrawer({ {/* Configuration Info Message - Show when data is missing */} - {/* Request/Response JSON - Using Tabs with View Toggle */} + {/* Request/Response JSON - Collapsible */} copyToClipboard(JSON.stringify(data, null, 2), label)} getRawRequest={getRawRequest} getFormattedResponse={getFormattedResponse} /> {/* Guardrail Data - Show only if present */} - {hasGuardrailData && ( -
- -
- )} + {hasGuardrailData && } {/* Vector Store Request Data - Show only if present */} - {hasVectorStoreData && ( -
- -
- )} + {hasVectorStoreData && } {/* Metadata Card - Only show if there's metadata */} {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( @@ -251,8 +239,8 @@ function ErrorDescription({ errorInfo }: { errorInfo: any }) { function TagsSection({ tags }: { tags: Record }) { return ( -
- +
+ Tags
@@ -286,8 +274,9 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: metadata.additional_usage_values.cache_read_input_tokens > 0); return ( - - +
+ + )} - {metadata?.litellm_overhead_time_ms !== undefined && ( + {metadata?.litellm_overhead_time_ms !== undefined && metadata.litellm_overhead_time_ms !== null && ( {metadata.litellm_overhead_time_ms.toFixed(2)} ms @@ -331,30 +320,26 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: +
); } interface RequestResponseSectionProps { - activeTab: typeof TAB_REQUEST | typeof TAB_RESPONSE; - jsonViewMode: ViewMode; hasResponse: boolean; - onTabChange: (key: typeof TAB_REQUEST | typeof TAB_RESPONSE) => void; - onViewModeChange: (mode: ViewMode) => void; onCopy: (data: any, label: string) => void; getRawRequest: () => any; getFormattedResponse: () => any; } function RequestResponseSection({ - activeTab, - jsonViewMode, hasResponse, - onTabChange, - onViewModeChange, onCopy, getRawRequest, getFormattedResponse, }: RequestResponseSectionProps) { + const [activeTab, setActiveTab] = useState(TAB_REQUEST); + const [isOpen, setIsOpen] = useState(true); + const handleCopy = () => { const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); const label = activeTab === TAB_REQUEST ? "Request" : "Response"; @@ -362,98 +347,109 @@ function RequestResponseSection({ }; return ( - - onTabChange(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} - tabBarExtraContent={ -
- {/* View Mode Toggle */} - onViewModeChange(e.target.value)}> - Formatted - JSON - - - {/* Copy Button */} - -
- } +
+ setIsOpen(keys.includes("request-response"))} + expandIcon={({ isActive }) => } + bordered={false} items={[ { - key: TAB_REQUEST, - label: "Request", + key: "request-response", + label: Request & Response, children: ( -
- -
- ), - }, - { - key: TAB_RESPONSE, - label: "Response", - children: ( -
- {hasResponse ? ( - - ) : ( -
- Response data not available -
- )} -
+ setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} + tabBarExtraContent={ + + } + items={[ + { + key: TAB_REQUEST, + label: "Request", + children: ( +
+ +
+ ), + }, + { + key: TAB_RESPONSE, + label: "Response", + children: ( +
+ {hasResponse ? ( + + ) : ( +
+ Response data not available +
+ )} +
+ ), + }, + ]} + /> ), }, ]} - style={{ padding: `0 ${SPACING_XLARGE}px` }} + styles={{ + header: { + padding: "16px", + borderBottom: "1px solid #f0f0f0", + }, + body: { + padding: 0, + }, + }} /> - +
); } function MetadataSection({ metadata, onCopy }: { metadata: Record; onCopy: (data: string) => void }) { return ( - } - onClick={() => onCopy(JSON.stringify(metadata, null, 2))} - > - Copy - - } - > -
+      }
+            onClick={() => onCopy(JSON.stringify(metadata, null, 2))}
+          >
+            Copy
+          
+        }
       >
-        {JSON.stringify(metadata, null, 2)}
-      
-
+
+          {JSON.stringify(metadata, null, 2)}
+        
+
+
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx index a9a30e55f17..5eec3c0a5cb 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/TokenFlow.tsx @@ -1,5 +1,4 @@ import { Typography } from "antd"; -import { COLOR_SECONDARY, FONT_FAMILY_MONO, FONT_SIZE_MEDIUM, SPACING_SMALL, SPACING_MEDIUM } from "./constants"; const { Text } = Typography; @@ -10,18 +9,14 @@ interface TokenFlowProps { } /** - * Displays token usage in a flow format: "prompt → completion (Σ total)" - * Makes it easy to see the relationship between input, output, and total tokens. + * Displays token usage in LiteLLM format: "12 (9 prompt tokens + 3 completion tokens)" + * Shows total with breakdown of prompt and completion tokens. */ export function TokenFlow({ prompt = 0, completion = 0, total = 0 }: TokenFlowProps) { return ( - - {prompt.toLocaleString()} - - {completion.toLocaleString()} - - (Σ {total.toLocaleString()}) - + + {total.toLocaleString()} ({prompt.toLocaleString()} prompt tokens + {completion.toLocaleString()} completion + tokens) ); } diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts index e1222ce00ae..91f5ff8f118 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/constants.ts @@ -9,14 +9,10 @@ export const API_BASE_MAX_WIDTH = 200; export const JSON_MAX_HEIGHT = 400; export const METADATA_MAX_HEIGHT = 300; -// Tab keys +// Tab keys (kept for backwards compatibility if needed) export const TAB_REQUEST = "request" as const; export const TAB_RESPONSE = "response" as const; -// View modes -export const VIEW_MODE_FORMATTED = "formatted" as const; -export const VIEW_MODE_JSON = "json" as const; - // Keyboard shortcuts export const KEY_ESCAPE = "Escape"; export const KEY_J_LOWER = "j"; From 2014bcf9d80e89b43e65cc2db70952c71ddf5466 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 15:44:49 -0800 Subject: [PATCH 05/11] fixes ui --- .../view_logs/CostBreakdownViewer.tsx | 2 +- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 134 ++++++++---------- 2 files changed, 62 insertions(+), 74 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx index affe28e0b25..7a02b89891b 100644 --- a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx @@ -59,7 +59,7 @@ export const CostBreakdownViewer: React.FC = ({ } return ( -
+
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 7b6e304e970..1d9af265350 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; -import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse } from "antd"; -import { CopyOutlined, DownOutlined } from "@ant-design/icons"; +import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message } from "antd"; +import { CopyOutlined } from "@ant-design/icons"; +import { Accordion, AccordionHeader, AccordionBody } from "@tremor/react"; import moment from "moment"; import { LogEntry } from "../columns"; import { formatNumberWithCommas } from "@/utils/dataUtils"; @@ -150,7 +151,7 @@ export function LogDetailsDrawer({ showIcon message="Request Failed" description={} - style={{ marginBottom: SPACING_XLARGE }} + className="mb-6" /> )} @@ -160,7 +161,7 @@ export function LogDetailsDrawer({ )} {/* Request Details Section */} -
+
{logEntry.model} @@ -191,7 +192,11 @@ export function LogDetailsDrawer({ {/* Configuration Info Message - Show when data is missing */} - + {missingData && ( +
+ +
+ )} {/* Request/Response JSON - Collapsible */} }) { return ( -
+
Tags @@ -274,7 +279,7 @@ function MetricsSection({ logEntry, metadata }: { logEntry: LogEntry; metadata: metadata.additional_usage_values.cache_read_input_tokens > 0); return ( -
+
@@ -338,7 +343,6 @@ function RequestResponseSection({ getFormattedResponse, }: RequestResponseSectionProps) { const [activeTab, setActiveTab] = useState(TAB_REQUEST); - const [isOpen, setIsOpen] = useState(true); const handleCopy = () => { const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); @@ -347,78 +351,62 @@ function RequestResponseSection({ }; return ( -
- setIsOpen(keys.includes("request-response"))} - expandIcon={({ isActive }) => } - bordered={false} - items={[ - { - key: "request-response", - label: Request & Response, - children: ( - setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} - tabBarExtraContent={ - - } - items={[ - { - key: TAB_REQUEST, - label: "Request", - children: ( -
- +
+ + +

Request & Response

+
+ + setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} + tabBarExtraContent={ + + } + items={[ + { + key: TAB_REQUEST, + label: "Request", + children: ( +
+ +
+ ), + }, + { + key: TAB_RESPONSE, + label: "Response", + children: ( +
+ {hasResponse ? ( + + ) : ( +
+ Response data not available
- ), - }, - { - key: TAB_RESPONSE, - label: "Response", - children: ( -
- {hasResponse ? ( - - ) : ( -
- Response data not available -
- )} -
- ), - }, - ]} - /> - ), - }, - ]} - styles={{ - header: { - padding: "16px", - borderBottom: "1px solid #f0f0f0", - }, - body: { - padding: 0, - }, - }} - /> + )} +
+ ), + }, + ]} + /> +
+
); } function MetadataSection({ metadata, onCopy }: { metadata: Record; onCopy: (data: string) => void }) { return ( -
+
Date: Fri, 30 Jan 2026 15:46:30 -0800 Subject: [PATCH 06/11] complete v2 viewer --- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 89 ++++++++++--------- 1 file changed, 47 insertions(+), 42 deletions(-) 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 1d9af265350..733cccdf1c9 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -143,7 +143,7 @@ export function LogDetailsDrawer({ environment={environment} /> -
+
{/* Error Alert - Show prominently at top for failures */} {hasError && errorInfo && ( 0 && ( copyToClipboard(data, "Metadata")} /> )} + + {/* Bottom spacing for scroll area */} +
); @@ -357,47 +360,49 @@ function RequestResponseSection({

Request & Response

- setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} - tabBarExtraContent={ - - } - items={[ - { - key: TAB_REQUEST, - label: "Request", - children: ( -
- -
- ), - }, - { - key: TAB_RESPONSE, - label: "Response", - children: ( -
- {hasResponse ? ( - - ) : ( -
- Response data not available -
- )} -
- ), - }, - ]} - /> +
+ setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} + tabBarExtraContent={ + + } + items={[ + { + key: TAB_REQUEST, + label: "Request", + children: ( +
+ +
+ ), + }, + { + key: TAB_RESPONSE, + label: "Response", + children: ( +
+ {hasResponse ? ( + + ) : ( +
+ Response data not available +
+ )} +
+ ), + }, + ]} + /> +
From 437e9e23bd2dc938a1c90b9a654abca762c91967 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 15:53:37 -0800 Subject: [PATCH 07/11] fix drawer --- .../LogDetailsDrawer/DrawerHeader.tsx | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx index 6b3f7fdaa5b..6aa6410b302 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/DrawerHeader.tsx @@ -139,7 +139,7 @@ function RequestIdSection({ requestId, onCopy }: { requestId: string; onCopy: () /** * Navigation controls (previous, next, close) - * Shows keyboard shortcuts with bounding boxes for visibility + * Shows keyboard shortcuts styled as buttons for visibility */ function NavigationSection({ onPrevious, @@ -150,14 +150,21 @@ function NavigationSection({ onNext: () => void; onClose: () => void; }) { - const keyboardShortcutStyle = { - border: "1px solid #d9d9d9", - borderRadius: 4, - padding: "0 4px", - fontSize: 12, + const keyboardShortcutStyle: React.CSSProperties = { + display: "inline-flex", + alignItems: "center", + justifyContent: "center", + minWidth: "20px", + height: "20px", + padding: "0 6px", + fontSize: 11, + fontWeight: 600, fontFamily: "monospace", marginLeft: 4, - background: "#fafafa", + background: "#fff", + border: "1px solid #d9d9d9", + borderRadius: 4, + boxShadow: "0 1px 2px rgba(0,0,0,0.05)", }; return ( From 9e1c76e9d0c6fdc14788e6f4950a882dc7e105c2 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 16:25:32 -0800 Subject: [PATCH 08/11] v1 - tool viewer in logs page --- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 1 + .../ToolsSection/FormattedToolView.tsx | 124 ++++++++ .../view_logs/ToolsSection/JsonToolView.tsx | 39 +++ .../ToolsSection/ToolExpandedContent.tsx | 52 ++++ .../view_logs/ToolsSection/ToolItem.tsx | 74 +++++ .../ToolsSection/ToolsSection.test.tsx | 117 +++++++ .../view_logs/ToolsSection/ToolsSection.tsx | 42 +++ .../view_logs/ToolsSection/index.ts | 7 + .../view_logs/ToolsSection/types.ts | 42 +++ .../view_logs/ToolsSection/utils.test.ts | 293 ++++++++++++++++++ .../view_logs/ToolsSection/utils.ts | 130 ++++++++ 11 files changed, 921 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts create mode 100644 ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts 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 733cccdf1c9..b3e59cf507f 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -27,6 +27,7 @@ import { SPACING_XLARGE, MESSAGE_REQUEST_ID_COPIED, } from "./constants"; +import { ToolsSection } from "../ToolsSection"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx new file mode 100644 index 00000000000..2f036a2a34a --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/FormattedToolView.tsx @@ -0,0 +1,124 @@ +/** + * Formatted view of tool definition with parameters table and call data + */ + +import { Typography, Table } from "antd"; +import { ParsedTool, ParameterRow } from "./types"; + +const { Text } = Typography; + +interface FormattedToolViewProps { + tool: ParsedTool; +} + +export function FormattedToolView({ tool }: FormattedToolViewProps) { + // Parse parameters for table display + const parameterRows: ParameterRow[] = Object.entries( + tool.parameters?.properties || {} + ).map(([name, schema]: [string, any]) => ({ + key: name, + name: name, + type: schema.type || "any", + description: schema.description || "-", + required: tool.parameters?.required?.includes(name) || false, + })); + + const columns = [ + { + title: "Parameter", + dataIndex: "name", + key: "name", + render: (name: string, record: ParameterRow) => ( + + {name} + {record.required && *} + + ), + }, + { + title: "Type", + dataIndex: "type", + key: "type", + render: (type: string) => ( + + {type} + + ), + }, + { + title: "Description", + dataIndex: "description", + key: "description", + render: (desc: string) => {desc}, + }, + ]; + + return ( +
+ {/* Description */} + {tool.description && ( +
+ {tool.description} +
+ )} + + {/* Parameters Table */} + {parameterRows.length > 0 && ( +
+ + Parameters + + + + )} + + {/* If tool was called, show the arguments used */} + {tool.called && tool.callData && ( +
+ + Called With + +
+
+              {JSON.stringify(tool.callData.arguments, null, 2)}
+            
+
+
+ )} + + ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx new file mode 100644 index 00000000000..2a2ceb644dc --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/JsonToolView.tsx @@ -0,0 +1,39 @@ +/** + * JSON view of tool definition + */ + +import { ParsedTool } from "./types"; + +interface JsonToolViewProps { + tool: ParsedTool; +} + +export function JsonToolView({ tool }: JsonToolViewProps) { + // Reconstruct the original tool definition + const toolJson = { + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.parameters, + }, + }; + + return ( +
+      {JSON.stringify(toolJson, null, 2)}
+    
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx new file mode 100644 index 00000000000..3c06dc7f08c --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolExpandedContent.tsx @@ -0,0 +1,52 @@ +/** + * Expanded content for a tool with view mode toggle + */ + +import { useState } from "react"; +import { Typography, Radio } from "antd"; +import { ParsedTool } from "./types"; +import { FormattedToolView } from "./FormattedToolView"; +import { JsonToolView } from "./JsonToolView"; + +const { Text } = Typography; + +type ViewMode = "formatted" | "json"; + +interface ToolExpandedContentProps { + tool: ParsedTool; +} + +export function ToolExpandedContent({ tool }: ToolExpandedContentProps) { + const [viewMode, setViewMode] = useState("formatted"); + + return ( +
+ {/* View Mode Toggle - Top Right */} +
+ + Description + + setViewMode(e.target.value)} + > + Formatted + JSON + +
+ + {viewMode === "formatted" ? ( + + ) : ( + + )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx new file mode 100644 index 00000000000..a5962a387af --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolItem.tsx @@ -0,0 +1,74 @@ +/** + * Individual tool item component with expandable details + */ + +import { useState } from "react"; +import { Typography, Tag } from "antd"; +import { ToolOutlined, RightOutlined, DownOutlined } from "@ant-design/icons"; +import { ParsedTool } from "./types"; +import { ToolExpandedContent } from "./ToolExpandedContent"; + +const { Text } = Typography; + +interface ToolItemProps { + tool: ParsedTool; +} + +export function ToolItem({ tool }: ToolItemProps) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ {/* Header Row - Always Visible */} +
setExpanded(!expanded)} + style={{ + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: "12px 16px", + cursor: "pointer", + background: expanded ? "#fafafa" : "#fff", + transition: "background 0.2s", + }} + > +
+ + + {tool.index}. {tool.name} + +
+ +
+ + {tool.called ? "called" : "not called"} + + {expanded ? ( + + ) : ( + + )} +
+
+ + {/* Expanded Content */} + {expanded && ( +
+ +
+ )} +
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx new file mode 100644 index 00000000000..753a552b6db --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx @@ -0,0 +1,117 @@ +/** + * Core tests for Tools section + */ + +import { describe, it, expect } from "vitest"; +import { parseToolsFromLog } from "./utils"; +import { LogEntry } from "../columns"; + +describe("ToolsSection", () => { + it("should parse tools from request and match with response tool calls", () => { + const mockLog: LogEntry = { + request_id: "test-123", + api_key: "key", + team_id: "team", + model: "gpt-4", + model_id: "gpt-4", + call_type: "completion", + spend: 0.01, + total_tokens: 100, + prompt_tokens: 50, + completion_tokens: 50, + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-01T00:00:01Z", + cache_hit: "none", + messages: JSON.stringify({ + model: "gpt-4", + messages: [{ role: "user", content: "What's the weather?" }], + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + required: ["location"], + properties: { + location: { type: "string", description: "City name" }, + }, + }, + }, + }, + { + type: "function", + function: { + name: "search_web", + description: "Search the web", + parameters: { + type: "object", + required: ["query"], + properties: { + query: { type: "string", description: "Search query" }, + }, + }, + }, + }, + ], + }), + response: JSON.stringify({ + choices: [ + { + message: { + tool_calls: [ + { + id: "call_123", + type: "function", + function: { + name: "get_weather", + arguments: '{"location": "San Francisco"}', + }, + }, + ], + }, + }, + ], + }), + }; + + const tools = parseToolsFromLog(mockLog); + + expect(tools).toHaveLength(2); + expect(tools[0].name).toBe("get_weather"); + expect(tools[0].called).toBe(true); + expect(tools[0].callData?.arguments).toEqual({ location: "San Francisco" }); + expect(tools[1].name).toBe("search_web"); + expect(tools[1].called).toBe(false); + }); + + it("should return empty array when no tools in request", () => { + const mockLog: LogEntry = { + request_id: "test-456", + api_key: "key", + team_id: "team", + model: "gpt-4", + model_id: "gpt-4", + call_type: "completion", + spend: 0.01, + total_tokens: 100, + prompt_tokens: 50, + completion_tokens: 50, + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-01T00:00:01Z", + cache_hit: "none", + messages: JSON.stringify({ + model: "gpt-4", + messages: [{ role: "user", content: "Hello" }], + }), + response: JSON.stringify({ + choices: [{ message: { content: "Hi there!" } }], + }), + }; + + const tools = parseToolsFromLog(mockLog); + + expect(tools).toHaveLength(0); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx new file mode 100644 index 00000000000..fdeec2b249d --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx @@ -0,0 +1,42 @@ +/** + * Tools section component that displays all available tools from the request + * and indicates which ones were actually called in the response + */ + +import { Typography } from "antd"; +import { LogEntry } from "../columns"; +import { parseToolsFromLog } from "./utils"; +import { ToolItem } from "./ToolItem"; + +const { Text } = Typography; + +interface ToolsSectionProps { + log: LogEntry; +} + +export function ToolsSection({ log }: ToolsSectionProps) { + const tools = parseToolsFromLog(log); + + // Don't render if no tools + if (tools.length === 0) return null; + + return ( +
+ + Tools + +
+ {tools.map((tool) => ( + + ))} +
+
+ ); +} diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts new file mode 100644 index 00000000000..e3b8600b003 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/index.ts @@ -0,0 +1,7 @@ +/** + * Export main components and utilities for the Tools section + */ + +export { ToolsSection } from "./ToolsSection"; +export { parseToolsFromLog, hasTools } from "./utils"; +export type { ParsedTool, ToolDefinition, ToolCall } from "./types"; diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts new file mode 100644 index 00000000000..92282fd1ca3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/types.ts @@ -0,0 +1,42 @@ +/** + * Type definitions for the Tools section + */ + +export interface ToolDefinition { + type: string; + function: { + name: string; + description?: string; + parameters?: Record; + }; +} + +export interface ToolCall { + id: string; + type: string; + function: { + name: string; + arguments: string; + }; +} + +export interface ParsedTool { + index: number; + name: string; + description: string; + parameters: Record; + called: boolean; + callData?: { + id: string; + name: string; + arguments: Record; + }; +} + +export interface ParameterRow { + key: string; + name: string; + type: string; + description: string; + required: boolean; +} diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts new file mode 100644 index 00000000000..75f975e9a13 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.test.ts @@ -0,0 +1,293 @@ +/** + * Tests for tool parsing utilities + */ + +import { describe, it, expect } from "vitest"; +import { parseToolsFromLog, hasTools } from "./utils"; +import { LogEntry } from "../columns"; + +describe("ToolsSection utils", () => { + describe("parseToolsFromLog", () => { + it("should return empty array when no tools in request", () => { + const log: Partial = { + request_id: "test-1", + messages: [], + response: {}, + }; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toEqual([]); + }); + + it("should parse tools from proxy_server_request", () => { + const log: Partial = { + request_id: "test-2", + proxy_server_request: { + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get the current weather", + parameters: { + type: "object", + properties: { + location: { type: "string" }, + }, + required: ["location"], + }, + }, + }, + ], + }, + response: {}, + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + index: 1, + name: "get_weather", + description: "Get the current weather", + called: false, + }); + }); + + it("should parse tools from messages object format", () => { + const log: Partial = { + request_id: "test-3", + messages: { + tools: [ + { + type: "function", + function: { + name: "search_web", + description: "Search the web", + }, + }, + ], + }, + response: {}, + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe("search_web"); + }); + + it("should mark tools as called when present in response", () => { + const log: Partial = { + request_id: "test-4", + proxy_server_request: { + tools: [ + { + type: "function", + function: { + name: "get_weather", + description: "Get weather", + }, + }, + { + type: "function", + function: { + name: "send_email", + description: "Send email", + }, + }, + ], + }, + response: { + choices: [ + { + message: { + tool_calls: [ + { + id: "call_123", + type: "function", + function: { + name: "get_weather", + arguments: '{"location": "San Francisco"}', + }, + }, + ], + }, + }, + ], + }, + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(2); + expect(result[0].called).toBe(true); + expect(result[0].callData).toBeDefined(); + expect(result[0].callData?.arguments).toEqual({ + location: "San Francisco", + }); + expect(result[1].called).toBe(false); + expect(result[1].callData).toBeUndefined(); + }); + + it("should handle string format request and response", () => { + const log: Partial = { + request_id: "test-5", + proxy_server_request: JSON.stringify({ + tools: [ + { + type: "function", + function: { + name: "calculate", + }, + }, + ], + }), + response: JSON.stringify({ + choices: [ + { + message: { + tool_calls: [ + { + id: "call_456", + type: "function", + function: { + name: "calculate", + arguments: '{"x": 5}', + }, + }, + ], + }, + }, + ], + }), + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(1); + expect(result[0].called).toBe(true); + }); + + it("should handle tools with no description or parameters", () => { + const log: Partial = { + request_id: "test-6", + proxy_server_request: { + tools: [ + { + type: "function", + function: { + name: "minimal_tool", + }, + }, + ], + }, + response: {}, + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(1); + expect(result[0]).toMatchObject({ + index: 1, + name: "minimal_tool", + description: "", + parameters: {}, + called: false, + }); + }); + + it("should handle invalid JSON in tool call arguments gracefully", () => { + const log: Partial = { + request_id: "test-7", + proxy_server_request: { + tools: [ + { + type: "function", + function: { + name: "test_tool", + }, + }, + ], + }, + response: { + choices: [ + { + message: { + tool_calls: [ + { + id: "call_789", + type: "function", + function: { + name: "test_tool", + arguments: "invalid json", + }, + }, + ], + }, + }, + ], + }, + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(1); + expect(result[0].called).toBe(true); + expect(result[0].callData?.arguments).toEqual({}); + }); + + it("should assign correct indices to multiple tools", () => { + const log: Partial = { + request_id: "test-8", + proxy_server_request: { + tools: [ + { type: "function", function: { name: "tool1" } }, + { type: "function", function: { name: "tool2" } }, + { type: "function", function: { name: "tool3" } }, + ], + }, + response: {}, + } as any; + + const result = parseToolsFromLog(log as LogEntry); + + expect(result).toHaveLength(3); + expect(result[0].index).toBe(1); + expect(result[1].index).toBe(2); + expect(result[2].index).toBe(3); + }); + }); + + describe("hasTools", () => { + it("should return false when no tools in request", () => { + const log: Partial = { + request_id: "test-9", + messages: [], + response: {}, + }; + + expect(hasTools(log as LogEntry)).toBe(false); + }); + + it("should return true when tools present in request", () => { + const log: Partial = { + request_id: "test-10", + proxy_server_request: { + tools: [ + { + type: "function", + function: { + name: "test_tool", + }, + }, + ], + }, + response: {}, + } as any; + + expect(hasTools(log as LogEntry)).toBe(true); + }); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts new file mode 100644 index 00000000000..33b21297c43 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/utils.ts @@ -0,0 +1,130 @@ +/** + * Utility functions for parsing and processing tool data from log entries + */ + +import { LogEntry } from "../columns"; +import { ParsedTool, ToolDefinition, ToolCall } from "./types"; + +/** + * Parse raw data that might be a string or object + */ +function parseData(input: any): any { + if (typeof input === "string") { + try { + return JSON.parse(input); + } catch { + return input; + } + } + return input; +} + +/** + * Extract tools array from request data + */ +function extractToolsFromRequest(log: LogEntry): ToolDefinition[] { + // Check proxy_server_request first (most complete), then messages + const requestData = parseData(log.proxy_server_request || log.messages); + + if (!requestData) return []; + + // Handle array format (messages array) + if (Array.isArray(requestData)) { + // Tools are not typically in messages array, return empty + return []; + } + + // Handle object format (request body) + if (typeof requestData === "object" && requestData.tools) { + return Array.isArray(requestData.tools) ? requestData.tools : []; + } + + return []; +} + +/** + * Extract tool calls from response data + */ +function extractToolCallsFromResponse(log: LogEntry): ToolCall[] { + const responseData = parseData(log.response); + + if (!responseData || typeof responseData !== "object") return []; + + // OpenAI format: response.choices[0].message.tool_calls + const choices = responseData.choices; + if (Array.isArray(choices) && choices.length > 0) { + const firstChoice = choices[0]; + const message = firstChoice.message; + if (message && Array.isArray(message.tool_calls)) { + return message.tool_calls; + } + } + + return []; +} + +/** + * Parse safe JSON with fallback + */ +function parseSafeJson(jsonString: string): Record { + try { + return JSON.parse(jsonString); + } catch { + return {}; + } +} + +/** + * Main function to parse tools from a log entry + * Returns an array of tools with their definition and call status + */ +export function parseToolsFromLog(log: LogEntry): ParsedTool[] { + // Get tools from request + const requestTools = extractToolsFromRequest(log); + + if (requestTools.length === 0) { + return []; + } + + // Get tool calls from response + const toolCalls = extractToolCallsFromResponse(log); + const calledToolNames = new Set( + toolCalls.map((tc: ToolCall) => tc.function?.name).filter(Boolean) + ); + + // Map tool calls by name for quick lookup + const toolCallMap = new Map(); + toolCalls.forEach((tc: ToolCall) => { + const name = tc.function?.name; + if (name) { + toolCallMap.set(name, { + id: tc.id, + name: name, + arguments: parseSafeJson(tc.function?.arguments || "{}"), + }); + } + }); + + // Parse each tool definition + return requestTools.map((tool: ToolDefinition, index: number) => { + const func = tool.function || { name: `Tool ${index + 1}` }; + const name = func.name || `Tool ${index + 1}`; + + return { + index: index + 1, + name: name, + description: func.description || "", + parameters: func.parameters || {}, + called: calledToolNames.has(name), + callData: toolCallMap.get(name), + }; + }); +} + +/** + * Check if a log entry has any tools + */ +export function hasTools(log: LogEntry): boolean { + const requestTools = extractToolsFromRequest(log); + return requestTools.length > 0; +} From 8890b9e4066e48a21813df7eae3df2f84854dc98 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 16:30:57 -0800 Subject: [PATCH 09/11] add preview for tool sections --- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 3 + .../view_logs/ToolsSection/ToolsSection.tsx | 56 +++++++++++++------ 2 files changed, 42 insertions(+), 17 deletions(-) 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 b3e59cf507f..cb7f2e2558e 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailsDrawer.tsx @@ -192,6 +192,9 @@ export function LogDetailsDrawer({ {/* Cost Breakdown - Show if cost breakdown data is available */} + {/* Tools Section - Show if tools are present in request */} + + {/* Configuration Info Message - Show when data is missing */} {missingData && (
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx index fdeec2b249d..60bcce214af 100644 --- a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx @@ -3,7 +3,7 @@ * and indicates which ones were actually called in the response */ -import { Typography } from "antd"; +import { Collapse, Typography } from "antd"; import { LogEntry } from "../columns"; import { parseToolsFromLog } from "./utils"; import { ToolItem } from "./ToolItem"; @@ -20,23 +20,45 @@ export function ToolsSection({ log }: ToolsSectionProps) { // Don't render if no tools if (tools.length === 0) return null; + // Calculate summary stats + const totalTools = tools.length; + const calledTools = tools.filter((t) => t.called).length; + + // Get preview of first 2 tool names + const toolNamePreview = tools + .slice(0, 2) + .map((t) => t.name) + .join(", "); + const hasMoreTools = tools.length > 2; + return ( -
- - Tools - -
- {tools.map((tool) => ( - - ))} -
+
+ +

Tools

+ + {totalTools} provided, {calledTools} called + + + • {toolNamePreview} + {hasMoreTools && "..."} + +
+ ), + children: ( +
+ {tools.map((tool) => ( + + ))} +
+ ), + }, + ]} + />
); } From 2d2b502b9db2a700651492d5ba9a2537abc33c43 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 16:35:06 -0800 Subject: [PATCH 10/11] ui fixes --- .../view_logs/CostBreakdownViewer.tsx | 36 ++-- .../GuardrailViewer/GuardrailViewer.tsx | 91 +++++---- .../LogDetailsDrawer/LogDetailsDrawer.tsx | 172 +++++++++--------- .../view_logs/ToolsSection/ToolsSection.tsx | 1 + .../view_logs/VectorStoreViewer.tsx | 37 ++-- 5 files changed, 169 insertions(+), 168 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx index 7a02b89891b..01cbd74c5a8 100644 --- a/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/CostBreakdownViewer.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Accordion, AccordionHeader, AccordionBody } from "@tremor/react"; +import { Collapse } from "antd"; import { formatNumberWithCommas } from "@/utils/dataUtils"; export interface CostBreakdown { @@ -60,18 +60,22 @@ export const CostBreakdownViewer: React.FC = ({ return (
- - -
-

Cost Breakdown

-
- Total: - {formatCost(totalSpend)} -
-
-
- -
+ +

Cost Breakdown

+
+ Total: + {formatCost(totalSpend)} +
+
+ ), + children: ( +
{/* Step 1: Base Token Costs */}
@@ -149,8 +153,10 @@ export const CostBreakdownViewer: React.FC = ({
-
-
+ ), + }, + ]} + />
); }; diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx index b25545200cd..206418fb314 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Tooltip } from "antd"; +import { Tooltip, Collapse } from "antd"; import PresidioDetectedEntities from "./PresidioDetectedEntities"; import BedrockGuardrailDetails, { BedrockGuardrailResponse, @@ -207,8 +207,6 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => { ? [data] : []; - const [sectionExpanded, setSectionExpanded] = useState(true); - const primaryName = guardrailEntries.length === 1 ? guardrailEntries[0].guardrail_name : `${guardrailEntries.length} guardrails`; const statuses = Array.from(new Set(guardrailEntries.map((entry) => entry.guardrail_status))); @@ -231,55 +229,50 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => { } return ( -
-
setSectionExpanded(!sectionExpanded)} - > -
- - - -

Guardrail Information

+
+ +

Guardrail Information

- - - {aggregatedStatus} - - + + + {aggregatedStatus} + + - {primaryName} + {primaryName} - {totalMaskedEntities > 0 && ( - - {totalMaskedEntities} masked {totalMaskedEntities === 1 ? "entity" : "entities"} - - )} -
- {sectionExpanded ? "Click to collapse" : "Click to expand"} -
- - {sectionExpanded && ( -
- {guardrailEntries.map((entry, index) => ( - - ))} -
- )} + {totalMaskedEntities > 0 && ( + + {totalMaskedEntities} masked {totalMaskedEntities === 1 ? "entity" : "entities"} + + )} +
+ ), + children: ( +
+ {guardrailEntries.map((entry, index) => ( + + ))} +
+ ), + }, + ]} + />
); }; 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 cb7f2e2558e..c88bf069965 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,6 @@ import { useState } from "react"; -import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message } from "antd"; +import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse } from "antd"; import { CopyOutlined } from "@ant-design/icons"; -import { Accordion, AccordionHeader, AccordionBody } from "@tremor/react"; import moment from "moment"; import { LogEntry } from "../columns"; import { formatNumberWithCommas } from "@/utils/dataUtils"; @@ -359,56 +358,61 @@ function RequestResponseSection({ return (
- - -

Request & Response

-
- -
- setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} - tabBarExtraContent={ - - } - items={[ - { - key: TAB_REQUEST, - label: "Request", - children: ( -
- -
- ), - }, - { - key: TAB_RESPONSE, - label: "Response", - children: ( -
- {hasResponse ? ( - - ) : ( -
- Response data not available + Request & Response, + children: ( +
+ setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} + tabBarExtraContent={ + + } + items={[ + { + key: TAB_REQUEST, + label: "Request", + children: ( +
+
- )} -
- ), - }, - ]} - /> -
- - + ), + }, + { + key: TAB_RESPONSE, + label: "Response", + children: ( +
+ {hasResponse ? ( + + ) : ( +
+ Response data not available +
+ )} +
+ ), + }, + ]} + /> +
+ ), + }, + ]} + />
); } @@ -416,36 +420,42 @@ function RequestResponseSection({ function MetadataSection({ metadata, onCopy }: { metadata: Record; onCopy: (data: string) => void }) { return (
- } - onClick={() => onCopy(JSON.stringify(metadata, null, 2))} - > - Copy - - } - > -
-          {JSON.stringify(metadata, null, 2)}
-        
-
+ Metadata, + children: ( +
+
+ +
+
+                  {JSON.stringify(metadata, null, 2)}
+                
+
+ ), + }, + ]} + />
); } diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx index 60bcce214af..7152db05599 100644 --- a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.tsx @@ -34,6 +34,7 @@ export function ToolsSection({ log }: ToolsSectionProps) { return (
>({}); if (!data || data.length === 0) { @@ -56,27 +56,15 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) { }; return ( -
-
setSectionExpanded(!sectionExpanded)} - > -
- - - -

Vector Store Requests

-
- {sectionExpanded ? "Click to collapse" : "Click to expand"} -
- - {sectionExpanded && ( -
+
+ Vector Store Requests, + children: ( +
{data.map((request, index) => (
@@ -168,7 +156,10 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
))}
- )} + ), + }, + ]} + />
); } From 2156db9f064082480fe865e2394b23edc2be5d1c Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Fri, 30 Jan 2026 16:37:33 -0800 Subject: [PATCH 11/11] new tool view --- .../GuardrailViewer/GuardrailViewer.test.tsx | 24 ++++++++++++------- .../GuardrailViewer/GuardrailViewer.tsx | 1 + .../LogDetailsDrawer/LogDetailsDrawer.tsx | 1 + .../view_logs/VectorStoreViewer.tsx | 1 + 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.test.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.test.tsx index 2dc3bdef97d..95120f60570 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { describe, it, expect, vi, beforeEach } from "vitest"; import userEvent from "@testing-library/user-event"; -import { renderWithProviders, screen } from "../../../../tests/test-utils"; +import { renderWithProviders, screen, waitFor } from "../../../../tests/test-utils"; import { makeBedrockResponse, makeEntity, @@ -62,20 +62,26 @@ describe("GuardrailViewer", () => { it("toggles main section open/closed and chevron rotation class", async () => { const user = userEvent.setup(); const data = makeGuardrailInformation(); - renderWithProviders(); + const { container } = renderWithProviders(); - const header = screen.getByText("Guardrail Information").closest("div")!; - // Initially expanded - expect(screen.getByText("Click to collapse")).toBeInTheDocument(); + const header = screen.getByText("Guardrail Information").closest(".ant-collapse-header")!; + // Initially expanded (content is visible) + expect(screen.getByText("Masked Entity Summary")).toBeInTheDocument(); + // Click to collapse await user.click(header); - expect(screen.getByText("Click to expand")).toBeInTheDocument(); - // Details gone - expect(screen.queryByText("Masked Entity Summary")).not.toBeInTheDocument(); + // Wait for collapse animation and content to be hidden + await waitFor(() => { + const contentBox = container.querySelector(".ant-collapse-content-box"); + expect(contentBox).not.toBeVisible(); + }); // Click to expand again await user.click(header); - expect(screen.getByText("Click to collapse")).toBeInTheDocument(); + // Wait for expand animation + await waitFor(() => { + expect(screen.getByText("Masked Entity Summary")).toBeVisible(); + }); }); it("defaults to presidio provider when guardrail_provider is undefined", async () => { diff --git a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx index 206418fb314..ed2198ba859 100644 --- a/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/GuardrailViewer/GuardrailViewer.tsx @@ -231,6 +231,7 @@ const GuardrailViewer = ({ data }: GuardrailViewerProps) => { return (
; return (