From 7c384f70cdf21ca87b63b29be48a1b51b8559f32 Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Wed, 30 Sep 2026 00:40:13 -0700 Subject: [PATCH] feat(ui): add span detail with LiteLLM request card --- .../view_logs/TraceView/SpanDetail.tsx | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/view_logs/TraceView/SpanDetail.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanDetail.tsx b/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanDetail.tsx new file mode 100644 index 00000000000..178f1edcdc8 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/TraceView/SpanDetail.tsx @@ -0,0 +1,235 @@ +"use client"; + +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/cva.config"; + +import { agentTraceSpanCall } from "../../networking"; +import { SPAN_PILL_CLASS, SpanStatusBadge, SpanTypePill } from "./TracePills"; +import type { LiteLLMRequest, Span, TraceMessage } from "./traceTypes"; +import { fmtCost, fmtMs, parseMessages, prettyPayload, spanLabel } from "./traceUtils"; + +interface SpanDetailProps { + accessToken: string; + traceId: string; + span: Span; + /** Open the existing request-log drawer for a LiteLLM request id. */ + onOpenRequestLog?: (requestId: string) => void; +} + +const LONG_MESSAGE_CHARS = 360; + +function Field({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { + return ( +
+
{label}
+
+ {value || "—"} +
+
+ ); +} + +function TokenFlowBar({ request }: { request: LiteLLMRequest }) { + const fresh = Math.max(request.prompt_tokens - request.cache_read_tokens - request.cache_write_tokens, 0); + const total = request.prompt_tokens + request.completion_tokens || 1; + const parts = [ + { label: "input", value: fresh, className: "bg-info" }, + { label: "cache read", value: request.cache_read_tokens, className: "bg-cyan-500" }, + { label: "cache write", value: request.cache_write_tokens, className: "bg-violet-500" }, + { label: "output", value: request.completion_tokens, className: "bg-amber-500" }, + ]; + return ( + <> +
+ {parts.map((part) => ( + + ))} +
+
+ {parts.map((part) => ( + + + {part.label} {part.value.toLocaleString()} + + ))} +
+ + ); +} + +function LiteLLMRequestCard({ + request, + onOpenRequestLog, +}: { + request: LiteLLMRequest; + onOpenRequestLog?: (requestId: string) => void; +}) { + return ( +
+
+ + LiteLLM request · joined from spend log + + {onOpenRequestLog && ( + + )} +
+
+ + + + + + + + + +
+ +
+ ); +} + +function MessageBlock({ message }: { message: TraceMessage }) { + const long = (message.content || "").length > LONG_MESSAGE_CHARS; + const [expanded, setExpanded] = useState(!long); + return ( +
+ + {message.content && ( +
+ {message.content} +
+ )} + {(message.tool_calls ?? []).map((call, i) => ( +
+ {call.name}({JSON.stringify(call.args, null, 1)}) +
+ ))} +
+ ); +} + +function Payload({ title, value }: { title: string; value: string }) { + if (!value) return null; + const messages = parseMessages(value); + return ( +
+

+ {title} + {messages && messages.length > 1 ? ` · ${messages.length} messages` : ""} +

+ {messages ? ( +
+ {messages.map((message, i) => ( + + ))} +
+ ) : ( +
+          {prettyPayload(value)}
+        
+ )} +
+ ); +} + +/** Right pane: span header, joined LiteLLM request, lazily fetched input / output / attributes. */ +export function SpanDetail({ accessToken, traceId, span, onOpenRequestLog }: SpanDetailProps) { + const detailQuery = useQuery({ + queryKey: ["agentTraceSpan", traceId, span.span_id, accessToken], + queryFn: () => agentTraceSpanCall(accessToken, traceId, span.span_id), + staleTime: Infinity, + }); + const detail = detailQuery.data; + + return ( +
+
+ + {spanLabel(span)} + +
+
+ + start +{fmtMs(span.start_offset_ms)} + + + duration {fmtMs(span.duration_ms)} + + + agent {span.agent || "—"} + + + span {span.span_id} + +
+ {span.status === "error" && span.error && ( +
+ {span.error} +
+ )} + {span.litellm && } + {detailQuery.isLoading &&
Loading span input/output…
} + {detailQuery.isError && ( +
Could not load span details: {detailQuery.error.message}
+ )} + {detail && ( + <> + + +
+

OTEL attributes

+
+              {JSON.stringify(
+                { trace_id: traceId, span_id: span.span_id, parent_span_id: span.parent_span_id, ...detail.attributes },
+                null,
+                2,
+              )}
+            
+
+ + )} +
+ ); +}