From af1c2f1a952aae561601b5f5bf3de4e9e6f69e0e Mon Sep 17 00:00:00 2001 From: Ishaan Jaff Date: Sat, 3 Oct 2026 17:00:29 -0700 Subject: [PATCH] feat(lens): format review span previews as readable messages --- .../components/lens/model/spanPreview.test.ts | 51 +++++++++++++++ .../src/components/lens/model/spanPreview.ts | 65 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/lens/model/spanPreview.test.ts create mode 100644 ui/litellm-dashboard/src/components/lens/model/spanPreview.ts diff --git a/ui/litellm-dashboard/src/components/lens/model/spanPreview.test.ts b/ui/litellm-dashboard/src/components/lens/model/spanPreview.test.ts new file mode 100644 index 00000000000..b9c70cd146b --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/model/spanPreview.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vitest"; +import { spanPreviewLines } from "./spanPreview"; + +describe("span previews", () => { + it("shows the last chat message of an agent span as role: content, not raw JSON", () => { + const preview = + 'Input: [{"role": "system", "content": "You are a research assistant."}, {"role": "user", "content": "Find the median latency of the EU region."}]\nOutput: [{"role": "assistant", "content": "I could not find it."}]'; + expect(spanPreviewLines(preview)).toEqual([ + { label: "in", text: "user: Find the median latency of the EU region.", error: false }, + { label: "out", text: "assistant: I could not find it.", error: false }, + ]); + }); + + it("recovers the last message from truncated JSON with the omitted marker", () => { + const preview = + 'Input: [{"role": "system", "content": "You are a bill\n[... preview omitted; read this span for evidence ...]\nn"}, {"role": "user", "content": "Cancel C2473\'s subscript'; + expect(spanPreviewLines(preview)).toEqual([ + { label: "in", text: "user: Cancel C2473's subscript", error: false }, + ]); + }); + + it("keeps tool payloads compact and drops an unset status", () => { + const preview = 'Input: {"query":"latency"}\nOutput: {"results": []}\nStatus: STATUS_CODE_UNSET '; + expect(spanPreviewLines(preview)).toEqual([ + { label: "in", text: '{"query":"latency"}', error: false }, + { label: "out", text: '{"results":[]}', error: false }, + ]); + }); + + it("marks a failed span's output and shows its error", () => { + const preview = + 'Input: {"query":"q"}\nOutput: {"error": "500 search index unavailable"}\nStatus: STATUS_CODE_ERROR 500 search index unavailable'; + const lines = spanPreviewLines(preview); + expect(lines.at(-1)).toEqual({ label: "error", text: "500 search index unavailable", error: true }); + expect(lines.find((l) => l.label === "out")?.error).toBe(true); + }); + + it("keeps multi-line payloads inside their section and collapses whitespace", () => { + const preview = 'Output: {"content": "def total(items):\\n return 1"}\nmore text\nStatus: STATUS_CODE_OK'; + expect(spanPreviewLines(preview)).toEqual([ + { label: "out", text: '{"content": "def total(items):\\n return 1"} more text', error: false }, + ]); + }); + + it("passes through plain text without sections", () => { + expect(spanPreviewLines("500 Internal Server Error")).toEqual([ + { label: "", text: "500 Internal Server Error", error: false }, + ]); + expect(spanPreviewLines("")).toEqual([]); + }); +}); diff --git a/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts b/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts new file mode 100644 index 00000000000..216f20126ae --- /dev/null +++ b/ui/litellm-dashboard/src/components/lens/model/spanPreview.ts @@ -0,0 +1,65 @@ +import { parseJson, parseMessages, previewText } from "@/components/view_logs/TraceView/traceUtils"; + +export interface PreviewLine { + label: string; + text: string; + error: boolean; +} + +const SECTION = /^(Input|Output|Status): ?/; +const OMITTED = /\n?\[\.\.\. preview omitted; read this span for evidence \.\.\.\]\n?/g; +const OK_STATUS = /^STATUS_CODE_(UNSET|OK)\b/; +const ROLE_CONTENT = /"role":\s*"(\w+)",\s*"content":\s*"((?:[^"\\]|\\.)*)/g; + +function sections(preview: string): Readonly> { + const lines = preview.split("\n"); + const starts = lines.flatMap((line, index) => (SECTION.test(line) ? [index] : [])); + if (!starts.length) return { Body: preview }; + return Object.fromEntries( + starts.map((start, n) => { + const body = lines.slice(start, starts[n + 1] ?? lines.length).join("\n"); + const name = SECTION.exec(body)?.[1] ?? "Body"; + return [name, body.replace(SECTION, "").trim()]; + }), + ); +} + +function decode(escaped: string): string { + const parsed = parseJson(`"${escaped.replace(/\\u[0-9a-fA-F]{0,3}$|\\$/, "")}"`); + return typeof parsed === "string" ? parsed : escaped; +} + +function lastMessage(value: string): string | null { + const messages = parseMessages(value); + if (messages?.length) { + const last = messages.at(-1); + return last ? `${last.role}: ${last.content}` : null; + } + const matches = [...value.matchAll(ROLE_CONTENT)]; + const match = matches.at(-1); + return match ? `${match[1]}: ${decode(match[2])}` : null; +} + +function compactJson(value: string): string { + const parsed = parseJson(value); + if (parsed === null || typeof parsed !== "object") return value; + return JSON.stringify(parsed); +} + +function readable(value: string): string { + const joined = value.replace(OMITTED, " … "); + return (lastMessage(joined) ?? previewText(compactJson(joined))).replace(/\s+/g, " ").trim(); +} + +export function spanPreviewLines(preview: string): PreviewLine[] { + const parts = sections(preview); + const status = parts.Status ?? ""; + const failed = !!status && !OK_STATUS.test(status); + const content = [ + ...(parts.Body ? [{ label: "", text: readable(parts.Body), error: false }] : []), + ...(parts.Input ? [{ label: "in", text: readable(parts.Input), error: false }] : []), + ...(parts.Output ? [{ label: "out", text: readable(parts.Output), error: failed }] : []), + ...(failed ? [{ label: "error", text: status.replace(/^STATUS_CODE_ERROR\s*/, ""), error: true }] : []), + ]; + return content.filter((line) => line.text); +}