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({