From 4d4f3a5b568b02f6965c1de1d0fa25b3b3c75eb9 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 30 Sep 2026 00:40:12 -0700 Subject: [PATCH] feat(ui): add agent traces table --- .../view_logs/TraceView/AgentTracesTable.tsx | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesTable.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesTable.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesTable.tsx new file mode 100644 index 00000000000..4bfc85ef323 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/AgentTracesTable.tsx @@ -0,0 +1,259 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { Workflow } from "lucide-react"; +import { Fragment, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { formatCellDate } from "@/components/shared/table_cells/date_cell"; +import { cn } from "@/lib/cva.config"; + +import { agentTraceCall } from "../../networking"; +import { AgentTracePill, SpanStatusBadge, SpanTypePill } from "./TracePills"; +import type { Span, TraceSummary } from "./traceTypes"; +import { + agentBadgeLabel, + fmtCost, + fmtMs, + fmtTok, + llmSpans, + previewText, + shortId, + spanLabel, + summaryHasErrors, + traceDisplayName, +} from "./traceUtils"; + +interface AgentTracesTableProps { + accessToken: string; + traces: TraceSummary[]; + isLoading: boolean; + error: Error | null; + hasMore: boolean; + onLoadMore: () => void; + /** Open the trace drawer, optionally focused on one span. */ + onOpenTrace: (traceId: string, spanId?: string) => void; + /** Compact mode is used in "All" where the table sits above the request logs. */ + compact?: boolean; +} + +const COLUMNS = ["", "Time", "Type", "Status", "Trace", "Input", "Cost", "Duration", "Model", "Tokens"] as const; + +const TRUNC = "block max-w-[280px] truncate"; + +function ChildRows({ + accessToken, + traceId, + onOpenTrace, +}: { + accessToken: string; + traceId: string; + onOpenTrace: AgentTracesTableProps["onOpenTrace"]; +}) { + const traceQuery = useQuery({ + queryKey: ["agentTrace", traceId, accessToken], + queryFn: () => agentTraceCall(accessToken, traceId), + staleTime: 30_000, + }); + if (traceQuery.isLoading || traceQuery.isError) { + return ( + + + {traceQuery.isError ? `Could not load trace: ${traceQuery.error.message}` : "Loading LLM calls…"} + + + ); + } + const spans = llmSpans(traceQuery.data?.spans ?? []); + return ( + <> + {spans.map((span) => ( + onOpenTrace(traceId, span.span_id)} /> + ))} + + ); +} + +function ChildRow({ span, onClick }: { span: Span; onClick: () => void }) { + const tokens = span.input_tokens + span.output_tokens; + return ( + + + +{fmtMs(span.start_offset_ms)} + + + + + + + + {span.litellm ? ( + shortId(span.litellm.request_id, 22) + ) : ( + {span.agent} + )} + + + {previewText(span.input_preview)} + + {fmtCost(span.litellm?.spend)} + {fmtMs(span.duration_ms)} + {spanLabel(span)} + {fmtTok(tokens)} + + ); +} + +function TraceRow({ + trace, + expanded, + onToggle, + onOpen, +}: { + trace: TraceSummary; + expanded: boolean; + onToggle: () => void; + onOpen: () => void; +}) { + const failed = summaryHasErrors(trace); + // Roots that never closed report "unset"; with no errors the run reads as a success. + const status = failed ? "error" : "ok"; + const handleToggle = (event: React.MouseEvent) => { + event.stopPropagation(); + onToggle(); + }; + return ( + + + + + {formatCellDate(new Date(trace.start_time), "datetime")} + + + + + + {trace.error_count > 0 && {trace.error_count} err} + + +
{traceDisplayName(trace)}
+
{shortId(trace.trace_id)}
+
+ + + {previewText(trace.input_preview)} + + + {fmtCost(trace.spend)} + {fmtMs(trace.duration_ms)} + + {trace.models.join(", ")} + + + {fmtTok(trace.input_tokens + trace.output_tokens)}{" "} + + ({fmtTok(trace.input_tokens)}+{fmtTok(trace.output_tokens)}) + + +
+ ); +} + +function EmptyState() { + return ( +
+
+ +
+
No agent traces in this time range
+
+ Agent runs exported over OTLP to this proxy will appear here. +
+
+ ); +} + +/** Agent trace rows: violet badge, ▸ expands the trace's LLM calls, click opens the trace drawer. */ +export function AgentTracesTable({ + accessToken, + traces, + isLoading, + error, + hasMore, + onLoadMore, + onOpenTrace, + compact = false, +}: AgentTracesTableProps) { + const [expanded, setExpanded] = useState>(new Set()); + const toggle = (traceId: string) => + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(traceId)) next.delete(traceId); + else next.add(traceId); + return next; + }); + + if (compact && traces.length === 0) return null; + + return ( +
+ + + + {COLUMNS.map((column, i) => ( + + {column} + + ))} + + + + {traces.map((trace) => ( + + toggle(trace.trace_id)} + onOpen={() => onOpenTrace(trace.trace_id)} + /> + {expanded.has(trace.trace_id) && ( + + )} + + ))} + +
+ {isLoading &&
Loading agent traces…
} + {error &&
Could not load traces: {error.message}
} + {!isLoading && !error && traces.length === 0 && } + {hasMore && ( +
+ +
+ )} +
+ ); +}