From 74119b7c8f55463ab0060f6c24dd6eeb1dfaa56f Mon Sep 17 00:00:00 2001
From: Brennan Butler <64561607+brennanbutler01@users.noreply.github.com>
Date: Sun, 20 Sep 2026 08:34:28 +0700
Subject: [PATCH 1/3] fix(ui): render native Responses WebSocket log output
---
...> PrettyMessagesView.integration.test.tsx} | 54 +++++++++++++++
.../LogDetailsDrawer/PrettyMessagesView.tsx | 26 ++++---
.../ResponsesWebSocketPrettyView.tsx | 61 ++++++++++++++++
.../LogDetailsDrawer/prettyMessagesTypes.ts | 7 ++
.../prettyMessagesUtils.test.ts | 69 +++++++++++++++++++
.../LogDetailsDrawer/prettyMessagesUtils.ts | 59 ++++++++++++++++
6 files changed, 268 insertions(+), 8 deletions(-)
rename ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/{PrettyMessagesView.test.tsx => PrettyMessagesView.integration.test.tsx} (78%)
create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx
create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx
similarity index 78%
rename from ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx
rename to ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx
index 807aaeeae00..efd1a2ca995 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx
@@ -4,6 +4,60 @@ import { describe, it, expect } from "vitest";
import { PrettyMessagesView } from "./PrettyMessagesView";
describe("PrettyMessagesView", () => {
+ it.each([false, true])("renders separate WebSocket turns (wrapped: %s)", (wrapped) => {
+ const events = [
+ { type: "response.created", response: { id: "resp_1", output: [] } },
+ { type: "response.output_text.delta", delta: "First answer" },
+ {
+ type: "response.completed",
+ response: {
+ id: "resp_1",
+ output: [{ type: "message", content: [{ type: "output_text", text: "First answer" }] }],
+ },
+ },
+ {
+ type: "response.incomplete",
+ response: {
+ id: "resp_2",
+ incomplete_details: { reason: "max_output_tokens" },
+ output: [{ type: "function_call", call_id: "call_1", name: "get_weather", arguments: '{"city":"Hanoi"}' }],
+ },
+ },
+ ];
+ render(
+ ,
+ );
+ expect(screen.getByText("Check the weather")).toBeInTheDocument();
+ expect(screen.getAllByText("First answer")).toHaveLength(1);
+ expect(screen.getByText("Turn 1 · Completed")).toBeInTheDocument();
+ expect(screen.getByText("Turn 2 · Incomplete")).toBeInTheDocument();
+ expect(screen.getByText("max_output_tokens")).toBeInTheDocument();
+ expect(screen.getByText("get_weather")).toBeInTheDocument();
+ expect(screen.getAllByText("Tokens: 42")).toHaveLength(1);
+ expect(screen.getAllByText("Cost: $0.001000")).toHaveLength(1);
+ });
+
+ it("shows failed and unfinished WebSocket turns without inventing output", () => {
+ render(
+ ,
+ );
+ expect(screen.getByText("Turn 1 · Failed")).toBeInTheDocument();
+ expect(screen.getByText("Provider unavailable")).toBeInTheDocument();
+ expect(screen.getByText("Turn 2 · No terminal event recorded")).toBeInTheDocument();
+ });
+
it("should render the component for standard chat completions", () => {
const request = {
messages: [{ role: "user", content: "Hello" }],
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.tsx
index 7d7e1bca6e8..ea38ea51d38 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.tsx
@@ -4,14 +4,15 @@
* Detects realtime API responses and renders a specialized view.
*/
-import { parseMessages } from "./prettyMessagesUtils";
+import { parseMessages, parseResponsesWebSocketTurns } from "./prettyMessagesUtils";
+import { ResponsesWebSocketPrettyView } from "./ResponsesWebSocketPrettyView";
import { InputCard } from "./InputCard";
import { OutputCard } from "./OutputCard";
import { isRealtimeResponse, RealtimePrettyView } from "./RealtimePrettyView";
interface PrettyMessagesViewProps {
- request: any;
- response: any;
+ request: unknown;
+ response: unknown;
metrics?: {
prompt_tokens?: number;
completion_tokens?: number;
@@ -26,6 +27,7 @@ export function PrettyMessagesView({ request, response, metrics }: PrettyMessage
}
const { requestMessages, responseMessage } = parseMessages(request, response);
+ const turns = parseResponsesWebSocketTurns(response);
return (
@@ -33,11 +35,19 @@ export function PrettyMessagesView({ request, response, metrics }: PrettyMessage
{/* Output Card */}
-
+ {turns !== null ? (
+
+ ) : (
+
+ )}
);
}
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx
new file mode 100644
index 00000000000..3569697dd4b
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx
@@ -0,0 +1,61 @@
+import { useState } from "react";
+import { toast } from "@/lib/toast";
+import { ResponsesWebSocketTurn } from "./prettyMessagesTypes";
+import { SectionHeader } from "./SectionHeader";
+import { SimpleMessageBlock } from "./SimpleMessageBlock";
+
+interface ResponsesWebSocketPrettyViewProps {
+ turns: readonly ResponsesWebSocketTurn[];
+ completionTokens?: number;
+ outputCost?: number;
+}
+
+export function ResponsesWebSocketPrettyView({
+ turns,
+ completionTokens,
+ outputCost,
+}: ResponsesWebSocketPrettyViewProps) {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+ const copyOutput = async () => {
+ await navigator.clipboard.writeText(JSON.stringify(turns, null, 2));
+ toast.success("Output copied");
+ };
+ return (
+
+
setIsCollapsed(!isCollapsed)}
+ />
+ {!isCollapsed && (
+
+ {turns.map((turn, index) => (
+
+
+ Turn {index + 1} · {turn.status}
+
+ {turn.id && {turn.id}
}
+ {turn.detail && {turn.detail}
}
+ {turn.message ? (
+
+ ) : (
+ No output recorded for this turn
+ )}
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts
index 10f6cbe865d..47778e259b5 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesTypes.ts
@@ -31,3 +31,10 @@ export interface ParsedMessages {
requestMessages: ParsedMessage[];
responseMessage: ParsedMessage | null;
}
+
+export interface ResponsesWebSocketTurn {
+ id: string;
+ status: "Completed" | "Incomplete" | "Failed" | "Error" | "No terminal event recorded";
+ message: ParsedMessage | null;
+ detail: string;
+}
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts
new file mode 100644
index 00000000000..15e44916038
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, it } from "vitest";
+import { parseResponsesWebSocketTurns } from "./prettyMessagesUtils";
+
+describe("parseResponsesWebSocketTurns", () => {
+ it.each([
+ null,
+ {},
+ [],
+ { results: null },
+ { choices: [] },
+ { output: [] },
+ { results: [{ type: "response.done" }] },
+ { results: [null, "invalid", 42] },
+ ])("leaves non-Responses payloads to their existing renderer: %j", (payload) => {
+ expect(parseResponsesWebSocketTurns(payload)).toBeNull();
+ });
+
+ it("uses terminal output once, skipping deltas, created snapshots, and duplicate terminal IDs", () => {
+ const response = {
+ id: "resp_1",
+ output: [{ type: "message", content: [{ type: "output_text", text: "Final text" }] }],
+ };
+ expect(
+ parseResponsesWebSocketTurns([
+ null,
+ { type: "response.created", response },
+ { type: "response.output_text.delta", delta: "Final text" },
+ { type: "response.completed", response },
+ { type: "response.completed", response },
+ ]),
+ ).toEqual([
+ {
+ id: "resp_1",
+ status: "Completed",
+ detail: "",
+ message: { role: "assistant", content: "Final text", toolCalls: undefined },
+ },
+ ]);
+ });
+
+ it("keeps failed output and errors inspectable alongside an unfinished turn", () => {
+ const turns = parseResponsesWebSocketTurns({
+ results: [
+ {
+ type: "response.failed",
+ response: {
+ id: "failed",
+ error: { code: "server_error" },
+ output: [{ type: "message", content: [{ type: "output_text", text: "Partial answer" }] }],
+ },
+ },
+ { type: "response.created", response: { id: "pending" } },
+ { type: "error", error: { message: "Connection error" } },
+ ],
+ });
+ expect(turns?.map(({ status, detail }) => ({ status, detail }))).toEqual([
+ { status: "Failed", detail: "server_error" },
+ { status: "No terminal event recorded", detail: "" },
+ { status: "Error", detail: "Connection error" },
+ ]);
+ expect(turns?.[0].message?.content).toBe("Partial answer");
+ });
+
+ it("does not invent output for malformed terminal payloads", () => {
+ expect(parseResponsesWebSocketTurns([{ type: "response.completed", response: null }])).toEqual([
+ { id: "", status: "Completed", message: null, detail: "" },
+ ]);
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
index 1f4289e7c2f..656698a762e 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
@@ -8,6 +8,7 @@ import {
ParsedMessages,
RequestPayload,
ResponsePayload,
+ ResponsesWebSocketTurn,
ToolCall,
} from "./prettyMessagesTypes";
@@ -52,6 +53,64 @@ export const parseMessages = (request: unknown, response: unknown): ParsedMessag
responseMessage: parseResponseMessage(classifyResponse(response)),
});
+export const parseResponsesWebSocketTurns = (response: unknown): ResponsesWebSocketTurn[] | null => {
+ const results: unknown = isRecord(response) ? response.results : response;
+ if (!Array.isArray(results)) return null;
+ const events = results.filter(isRecord);
+ if (
+ !events.some((event) =>
+ ["response.created", "response.completed", "response.incomplete", "response.failed"].includes(
+ asString(event.type),
+ ),
+ )
+ )
+ return null;
+ const terminalTypes = ["response.completed", "response.incomplete", "response.failed"];
+ const terminalStatuses: Readonly> = {
+ "response.completed": "Completed",
+ "response.incomplete": "Incomplete",
+ "response.failed": "Failed",
+ };
+ return events.flatMap((event, index): ResponsesWebSocketTurn[] => {
+ const body = isRecord(event.response) ? event.response : {};
+ const id = asString(body.id);
+ if (event.type === "response.created") {
+ const hasTerminal = events.some(
+ (other) =>
+ terminalTypes.includes(asString(other.type)) &&
+ isRecord(other.response) &&
+ asString(other.response.id) === id,
+ );
+ return hasTerminal ? [] : [{ id, status: "No terminal event recorded", message: null, detail: "" }];
+ }
+ if (event.type === "error") {
+ const error = isRecord(event.error) ? event.error : event;
+ return [{ id: "", status: "Error", message: null, detail: asString(error.message) || asString(error.code) }];
+ }
+ if (!terminalTypes.includes(asString(event.type))) return [];
+ if (
+ id &&
+ events
+ .slice(0, index)
+ .some(
+ (other) =>
+ terminalTypes.includes(asString(other.type)) && isRecord(other.response) && other.response.id === id,
+ )
+ )
+ return [];
+ const error = isRecord(body.error) ? body.error : {};
+ const incomplete = isRecord(body.incomplete_details) ? body.incomplete_details : {};
+ return [
+ {
+ id,
+ status: terminalStatuses[asString(event.type)],
+ message: parseResponseMessage(classifyResponse(body)),
+ detail: asString(error.message) || asString(error.code) || asString(incomplete.reason),
+ },
+ ];
+ });
+};
+
const parseRequestMessages = (payload: RequestPayload): ParsedMessage[] => {
switch (payload.kind) {
case "chat":
From 44b461fa2577fdccd3a20c8b10eace4ff2544101 Mon Sep 17 00:00:00 2001
From: Brennan Butler <64561607+brennanbutler01@users.noreply.github.com>
Date: Sun, 20 Sep 2026 09:01:43 +0700
Subject: [PATCH 2/3] fix(ui): bound WebSocket turn rendering and remove
quadratic scans
---
.../PrettyMessagesView.integration.test.tsx | 23 ++++++++++++-
.../ResponsesWebSocketPrettyView.tsx | 34 ++++++++++++++++---
.../prettyMessagesUtils.test.ts | 17 ++++++++++
.../LogDetailsDrawer/prettyMessagesUtils.ts | 27 ++++++---------
4 files changed, 79 insertions(+), 22 deletions(-)
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx
index efd1a2ca995..7282aa4d666 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/PrettyMessagesView.integration.test.tsx
@@ -1,9 +1,30 @@
import React from "react";
-import { render, screen } from "@testing-library/react";
+import { fireEvent, render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { PrettyMessagesView } from "./PrettyMessagesView";
describe("PrettyMessagesView", () => {
+ it("bounds rendered WebSocket turns and lets operators navigate all pages", () => {
+ const response = Array.from({ length: 101 }, (_, index) => ({
+ type: "response.completed",
+ response: { id: `resp_${index}`, output: [] },
+ }));
+ render();
+ expect(screen.getAllByRole("region", { name: /^Turn / })).toHaveLength(50);
+ expect(screen.getByText("Turns 1–50 of 101")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Previous turns" })).toBeDisabled();
+ fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
+ expect(screen.queryByText("Turn 1 · Completed")).not.toBeInTheDocument();
+ expect(screen.getByText("Turn 51 · Completed")).toBeInTheDocument();
+ expect(screen.getAllByRole("region", { name: /^Turn / })).toHaveLength(50);
+ fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
+ expect(screen.getByText("Turns 101–101 of 101")).toBeInTheDocument();
+ expect(screen.getAllByRole("region", { name: /^Turn / })).toHaveLength(1);
+ expect(screen.getByRole("button", { name: "Next turns" })).toBeDisabled();
+ fireEvent.click(screen.getByRole("button", { name: "Previous turns" }));
+ expect(screen.getByText("Turn 51 · Completed")).toBeInTheDocument();
+ });
+
it.each([false, true])("renders separate WebSocket turns (wrapped: %s)", (wrapped) => {
const events = [
{ type: "response.created", response: { id: "resp_1", output: [] } },
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx
index 3569697dd4b..4035e6204f6 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/ResponsesWebSocketPrettyView.tsx
@@ -3,6 +3,9 @@ import { toast } from "@/lib/toast";
import { ResponsesWebSocketTurn } from "./prettyMessagesTypes";
import { SectionHeader } from "./SectionHeader";
import { SimpleMessageBlock } from "./SimpleMessageBlock";
+import { Button } from "@/components/ui/button";
+
+const TURNS_PER_PAGE = 50;
interface ResponsesWebSocketPrettyViewProps {
turns: readonly ResponsesWebSocketTurn[];
@@ -16,6 +19,11 @@ export function ResponsesWebSocketPrettyView({
outputCost,
}: ResponsesWebSocketPrettyViewProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
+ const [page, setPage] = useState(0);
+ const lastPage = Math.max(0, Math.ceil(turns.length / TURNS_PER_PAGE) - 1);
+ const currentPage = Math.min(page, lastPage);
+ const start = currentPage * TURNS_PER_PAGE;
+ const end = Math.min(start + TURNS_PER_PAGE, turns.length);
const copyOutput = async () => {
await navigator.clipboard.writeText(JSON.stringify(turns, null, 2));
toast.success("Output copied");
@@ -32,14 +40,14 @@ export function ResponsesWebSocketPrettyView({
/>
{!isCollapsed && (
- {turns.map((turn, index) => (
+ {turns.slice(start, end).map((turn, index) => (
- Turn {index + 1} · {turn.status}
+ Turn {start + index + 1} · {turn.status}
{turn.id && {turn.id}
}
{turn.detail && {turn.detail}
}
@@ -54,6 +62,24 @@ export function ResponsesWebSocketPrettyView({
)}
))}
+ {turns.length > TURNS_PER_PAGE && (
+
+ )}
)}
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts
index 15e44916038..e7881a6f5c8 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.test.ts
@@ -2,6 +2,23 @@ import { describe, expect, it } from "vitest";
import { parseResponsesWebSocketTurns } from "./prettyMessagesUtils";
describe("parseResponsesWebSocketTurns", () => {
+ it("reads event types a bounded number of times for a large session", () => {
+ const reads = { count: 0 };
+ const events = Array.from({ length: 2000 }, (_, index) =>
+ ["response.created", "response.completed", "response.completed"].map((type) => ({
+ get type() {
+ reads.count += 1;
+ return type;
+ },
+ response: { id: `resp_${index}`, output: [] },
+ })),
+ ).flat();
+ const turns = parseResponsesWebSocketTurns(events);
+ expect(turns).toHaveLength(2000);
+ expect(turns?.[1999].id).toBe("resp_1999");
+ expect(reads.count).toBeLessThan(events.length * 20);
+ });
+
it.each([
null,
{},
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
index 656698a762e..a01382516c2 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/prettyMessagesUtils.ts
@@ -71,16 +71,17 @@ export const parseResponsesWebSocketTurns = (response: unknown): ResponsesWebSoc
"response.incomplete": "Incomplete",
"response.failed": "Failed",
};
- return events.flatMap((event, index): ResponsesWebSocketTurn[] => {
+ const terminalIds = new Set(
+ events.flatMap((event) =>
+ terminalTypes.includes(asString(event.type)) && isRecord(event.response) ? [asString(event.response.id)] : [],
+ ),
+ );
+ const seenTerminalIds = new Set();
+ return events.flatMap((event): ResponsesWebSocketTurn[] => {
const body = isRecord(event.response) ? event.response : {};
const id = asString(body.id);
if (event.type === "response.created") {
- const hasTerminal = events.some(
- (other) =>
- terminalTypes.includes(asString(other.type)) &&
- isRecord(other.response) &&
- asString(other.response.id) === id,
- );
+ const hasTerminal = terminalIds.has(id);
return hasTerminal ? [] : [{ id, status: "No terminal event recorded", message: null, detail: "" }];
}
if (event.type === "error") {
@@ -88,16 +89,8 @@ export const parseResponsesWebSocketTurns = (response: unknown): ResponsesWebSoc
return [{ id: "", status: "Error", message: null, detail: asString(error.message) || asString(error.code) }];
}
if (!terminalTypes.includes(asString(event.type))) return [];
- if (
- id &&
- events
- .slice(0, index)
- .some(
- (other) =>
- terminalTypes.includes(asString(other.type)) && isRecord(other.response) && other.response.id === id,
- )
- )
- return [];
+ if (id && seenTerminalIds.has(id)) return [];
+ if (id) seenTerminalIds.add(id);
const error = isRecord(body.error) ? body.error : {};
const incomplete = isRecord(body.incomplete_details) ? body.incomplete_details : {};
return [
From 840eeca2002a9b9e3dc5738781b6a736796ce585 Mon Sep 17 00:00:00 2001
From: Brennan Butler <64561607+brennanbutler01@users.noreply.github.com>
Date: Sun, 20 Sep 2026 09:07:36 +0700
Subject: [PATCH 3/3] fix(ui): reset response pagination when switching logs
---
.../LogDetailContent.integration.test.tsx | 29 ++++++++++++++++++-
.../LogDetailsDrawer/LogDetailContent.tsx | 1 +
2 files changed, 29 insertions(+), 1 deletion(-)
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.integration.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.integration.test.tsx
index 68336e80b0f..771a5d1c6af 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.integration.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.integration.test.tsx
@@ -1,4 +1,4 @@
-import { render, screen, within } from "@testing-library/react";
+import { fireEvent, render, screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it, vi } from "vitest";
import { GuardrailJumpLink, LogDetailContent } from "./LogDetailContent";
@@ -34,6 +34,33 @@ const createLogEntry = (overrides: Partial = {}): LogEntry =>
}) as LogEntry;
describe("LogDetailContent", () => {
+ it("resets WebSocket pagination for a different log but preserves it when the same log refreshes", () => {
+ const response = {
+ results: Array.from({ length: 101 }, (_, index) => ({
+ type: "response.completed",
+ response: { id: `resp_${index}`, output: [] },
+ })),
+ };
+ const log = createLogEntry({ request_id: "websocket-first", response });
+ const { rerender } = render();
+ fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
+ expect(screen.getByText("Turns 51–100 of 101")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("Turns 51–100 of 101")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("Turns 1–50 of 101")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: "Previous turns" })).toBeDisabled();
+ expect(screen.queryByText("Turn 51 · Completed")).not.toBeInTheDocument();
+ fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
+ fireEvent.click(screen.getByRole("button", { name: "Next turns" }));
+ expect(screen.getByText("Turns 101–101 of 101")).toBeInTheDocument();
+
+ rerender();
+ expect(screen.getByText("Turns 1–50 of 101")).toBeInTheDocument();
+ });
+
it("should render the component successfully", () => {
render();
diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
index 1a19c77d6f6..195683307c6 100644
--- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/LogDetailContent.tsx
@@ -658,6 +658,7 @@ function RequestResponseSection({