diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx new file mode 100644 index 00000000000..ef5de9febc3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx @@ -0,0 +1,442 @@ +import { useState } from "react"; +import { Typography, Descriptions, Card, Tag, Tabs, Alert, Collapse, Radio, Space } from "antd"; +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 } from "./JsonViewer"; +import { + formatData, + checkHasMessages, + checkHasResponse, + normalizeGuardrailEntries, + calculateTotalMaskedEntities, + getGuardrailLabel, + checkHasVectorStoreData, +} from "./utils"; +import { + DRAWER_CONTENT_PADDING, + API_BASE_MAX_WIDTH, + METADATA_MAX_HEIGHT, + TAB_REQUEST, + TAB_RESPONSE, + FONT_SIZE_SMALL, + FONT_FAMILY_MONO, + SPACING_XLARGE, + SPACING_MEDIUM, +} from "./constants"; +import { ToolsSection } from "../ToolsSection"; +import { PrettyMessagesView } from "./PrettyMessagesView"; + +const { Text } = Typography; + +export interface LogDetailContentProps { + logEntry: LogEntry; + onOpenSettings?: () => void; +} + +/** + * The scrollable detail content for a single log entry. + * Renders request details, metrics, cost breakdown, request/response, + * guardrails, vector store data, and metadata. + * + * Designed to be placed inside LogDetailsDrawer's right panel so it can + * be reused for both single-log and session-mode views. + */ +export function LogDetailContent({ logEntry, onOpenSettings }: LogDetailContentProps) { + const metadata = logEntry.metadata || {}; + const hasError = metadata.status === "failure"; + const errorInfo = hasError ? metadata.error_information : null; + + const hasMessages = checkHasMessages(logEntry.messages); + const hasResponse = checkHasResponse(logEntry.response); + const missingData = !hasMessages && !hasResponse && !hasError; + + // 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); + + 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 */} + {hasError && errorInfo && ( + } + className="mb-6" + /> + )} + + {/* Tags */} + {logEntry.request_tags && Object.keys(logEntry.request_tags).length > 0 && ( + + )} + + {/* Request Details */} +
+ + + {logEntry.model} + {logEntry.custom_llm_provider || "-"} + {logEntry.call_type} + + + + + + + {logEntry.requester_ip_address && ( + {logEntry.requester_ip_address} + )} + {hasGuardrailData && ( + + + + )} + + +
+ + {/* Metrics */} + + + {/* Cost Breakdown */} + + + {/* Tools */} + + + {/* Configuration Info Message */} + {missingData && ( +
+ +
+ )} + + {/* Request/Response JSON */} + + + {/* Guardrail Data */} + {hasGuardrailData && } + + {/* Vector Store Data */} + {hasVectorStoreData && } + + {/* Metadata */} + {logEntry.metadata && Object.keys(logEntry.metadata).length > 0 && ( + + )} + + {/* Bottom spacing */} +
+
+ ); +} + +// ============================================================================ +// 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 + + {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 !== null && ( + + {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 { + hasResponse: boolean; + hasError: boolean; + getRawRequest: () => any; + getFormattedResponse: () => any; + logEntry: LogEntry; +} + +function RequestResponseSection({ + hasResponse, + hasError, + getRawRequest, + getFormattedResponse, + logEntry, +}: RequestResponseSectionProps) { + const [activeTab, setActiveTab] = useState(TAB_REQUEST); + const [viewMode, setViewMode] = useState<'pretty' | 'json'>('pretty'); + + const getCopyText = () => { + const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse(); + return JSON.stringify(data, null, 2); + }; + + const totalSpend = logEntry.spend || 0; + const promptTokens = logEntry.prompt_tokens || 0; + const completionTokens = logEntry.completion_tokens || 0; + const totalTokens = promptTokens + completionTokens; + const inputCost = totalTokens > 0 ? (totalSpend * promptTokens) / totalTokens : 0; + const outputCost = totalTokens > 0 ? (totalSpend * completionTokens) / totalTokens : 0; + + return ( +
+ { + const target = e.target as HTMLElement; + if (target.closest('.ant-radio-group')) { + e.stopPropagation(); + } + }} + > +

Request & Response

+ setViewMode(e.target.value)} + > + Pretty + JSON + +
+ ), + children: ( +
+ {viewMode === 'pretty' ? ( + + ) : ( + setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)} + tabBarExtraContent={ + + } + items={[ + { + key: TAB_REQUEST, + label: "Request", + children: ( +
+ +
+ ), + }, + { + key: TAB_RESPONSE, + label: "Response", + children: ( +
+ {hasResponse || hasError ? ( + + ) : ( +
+ Response data not available +
+ )} +
+ ), + }, + ]} + /> + )} +
+ ), + }, + ]} + /> +
+ ); +} + +function MetadataSection({ metadata }: { metadata: Record }) { + return ( +
+ Metadata, + children: ( +
+
+ +
+
+                  {JSON.stringify(metadata, null, 2)}
+                
+
+ ), + }, + ]} + /> +
+ ); +}