Merge pull request #17233 from BerriAI/litellm_ui_logs_json

[Fix] Request and Response Panel JSONViewer
This commit is contained in:
yuneng-jiang
2025-11-27 15:19:52 -08:00
committed by GitHub
2 changed files with 169 additions and 20 deletions
@@ -0,0 +1,164 @@
import { render, screen, act } from "@testing-library/react";
import { describe, expect, it, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { RequestResponsePanel } from "./RequestResponsePanel";
import type { LogEntry } from "./columns";
import NotificationsManager from "../molecules/notifications_manager";
const mockNotificationsManager = vi.mocked(NotificationsManager);
const baseLogEntry: LogEntry = {
request_id: "chatcmpl-test-id",
api_key: "api-key",
team_id: "team-id",
model: "gpt-4",
model_id: "gpt-4",
call_type: "chat",
spend: 0,
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
startTime: "2025-11-14T00:00:00Z",
endTime: "2025-11-14T00:00:00Z",
cache_hit: "miss",
duration: 1,
messages: [{ role: "user", content: "hello" }],
response: { status: "ok" },
metadata: {
status: "success",
additional_usage_values: {
cache_read_input_tokens: 0,
cache_creation_input_tokens: 0,
},
},
request_tags: {},
custom_llm_provider: "openai",
api_base: "https://api.example.com",
};
describe("RequestResponsePanel", () => {
beforeEach(() => {
vi.clearAllMocks();
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: vi.fn().mockResolvedValue(undefined),
},
writable: true,
configurable: true,
});
Object.defineProperty(window, "isSecureContext", {
value: true,
writable: true,
configurable: true,
});
});
it("should render the component with request and response panels", () => {
const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" });
const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" });
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
expect(screen.getByText("Request")).toBeInTheDocument();
expect(screen.getByText("Response")).toBeInTheDocument();
});
it("should copy request to clipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request data" });
const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response" });
const mockWriteText = vi.fn().mockResolvedValue(undefined);
if (navigator.clipboard) {
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText);
} else {
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: mockWriteText,
},
writable: true,
configurable: true,
});
}
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
const copyButtons = screen.getAllByRole("button");
const copyRequestButton = copyButtons.find((button) => button.getAttribute("title") === "Copy request");
expect(copyRequestButton).toBeInTheDocument();
await act(async () => {
await user.click(copyRequestButton!);
});
expect(mockGetRawRequest).toHaveBeenCalled();
expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "request data" }, null, 2));
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Request copied to clipboard");
});
it("should copy response to clipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const mockGetRawRequest = vi.fn().mockReturnValue({ test: "request" });
const mockFormattedResponse = vi.fn().mockReturnValue({ test: "response data" });
const mockWriteText = vi.fn().mockResolvedValue(undefined);
if (navigator.clipboard) {
vi.spyOn(navigator.clipboard, "writeText").mockImplementation(mockWriteText);
} else {
Object.defineProperty(navigator, "clipboard", {
value: {
writeText: mockWriteText,
},
writable: true,
configurable: true,
});
}
render(
<RequestResponsePanel
row={{ original: baseLogEntry }}
hasMessages={true}
hasResponse={true}
hasError={false}
errorInfo={null}
getRawRequest={mockGetRawRequest}
formattedResponse={mockFormattedResponse}
/>,
);
const copyButtons = screen.getAllByRole("button");
const copyResponseButton = copyButtons.find((button) => button.getAttribute("title") === "Copy response");
expect(copyResponseButton).toBeInTheDocument();
expect(copyResponseButton).not.toBeDisabled();
await act(async () => {
await user.click(copyResponseButton!);
});
expect(mockFormattedResponse).toHaveBeenCalled();
expect(mockWriteText).toHaveBeenCalledWith(JSON.stringify({ test: "response data" }, null, 2));
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Response copied to clipboard");
});
});
@@ -1,6 +1,7 @@
import { LogEntry } from "./columns";
import NotificationsManager from "../molecules/notifications_manager";
import { JsonView, allExpanded, defaultStyles } from 'react-json-view-lite';
import { JsonView, defaultStyles } from "react-json-view-lite";
import "react-json-view-lite/dist/index.css";
interface RequestResponsePanelProps {
row: {
@@ -95,15 +96,7 @@ export function RequestResponsePanel({
</button>
</div>
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
<pre className="text-xs font-mono whitespace-pre-wrap break-all w-full max-w-full overflow-hidden break-words">
{/* {JSON.stringify(getRawRequest(), null, 2)} */}
<JsonView
data={getRawRequest()}
shouldExpandNode={allExpanded}
style={defaultStyles}
clickToExpandNode={true}
/>
</pre>
<JsonView data={getRawRequest()} style={defaultStyles} clickToExpandNode={true} />
</div>
</div>
@@ -136,17 +129,9 @@ export function RequestResponsePanel({
</svg>
</button>
</div>
<div className="p-4 overflow-auto max-h-96 bg-gray-50 w-full max-w-full box-border">
<div className="p-4 overflow-auto max-h-96 w-full max-w-full box-border">
{hasResponse ? (
<pre className="text-xs font-mono whitespace-pre-wrap break-all w-full max-w-full overflow-hidden break-words">
{/* {JSON.stringify(formattedResponse(), null, 2)} */}
<JsonView
data={formattedResponse()}
shouldExpandNode={allExpanded}
style={defaultStyles}
clickToExpandNode={true}
/>
</pre>
<JsonView data={formattedResponse()} style={defaultStyles} clickToExpandNode={true} />
) : (
<div className="text-gray-500 text-sm italic text-center py-4">Response data not available</div>
)}