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,
+ )}
+
+
+ >
+ )}
+
+ );
+}