diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index b9b27e6cbb5..269348ec054 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -1098,11 +1098,6 @@
"count": 1
}
},
- "src/app/(dashboard)/transform-request/TransformRequestPanel.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/app/(dashboard)/ui-theme/UIThemeSettings.tsx": {
"no-restricted-imports": {
"count": 1
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.test.tsx
new file mode 100644
index 00000000000..a0add153116
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.test.tsx
@@ -0,0 +1,160 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import TransformRequestPanel from "./TransformRequestPanel";
+import { transformRequestCall } from "@/components/networking";
+import NotificationsManager from "@/components/molecules/notifications_manager";
+
+vi.mock("@/components/networking", () => ({
+ transformRequestCall: vi.fn(),
+}));
+
+vi.mock("@/components/molecules/notifications_manager", () => ({
+ default: {
+ success: vi.fn(),
+ info: vi.fn(),
+ fromBackend: vi.fn(),
+ },
+}));
+
+const transformRequestCallMock = vi.mocked(transformRequestCall);
+const notify = vi.mocked(NotificationsManager);
+
+const ACCESS_TOKEN = "sk-test-token";
+
+const getRequestTextarea = () => screen.getByPlaceholderText(/press cmd\/ctrl \+ enter to transform/i);
+
+const getTransformButton = () => screen.getByRole("button", { name: /transform/i });
+
+const getCopyButton = () => screen.getByRole("button", { name: /copy to clipboard/i });
+
+describe("TransformRequestPanel", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it("renders both panels, the prefilled request and the placeholder curl", () => {
+ render();
+
+ expect(screen.getByText("Original Request")).toBeInTheDocument();
+ expect(screen.getByText("Transformed Request")).toBeInTheDocument();
+ expect(screen.getByText(/sensitive headers are not shown/i)).toBeInTheDocument();
+
+ expect((getRequestTextarea() as HTMLTextAreaElement).value).toContain('"model": "openai/gpt-4o"');
+ expect(screen.getByText(/https:\/\/api\.openai\.com\/v1\/chat\/completions/)).toBeInTheDocument();
+
+ expect(screen.getByRole("link", { name: /here/i })).toHaveAttribute(
+ "href",
+ "https://github.com/BerriAI/litellm/issues",
+ );
+ });
+
+ it("sends the edited request body as a completion call and renders the returned curl", async () => {
+ const user = userEvent.setup();
+ transformRequestCallMock.mockResolvedValue({
+ raw_request_api_base: "https://api.anthropic.com/v1/messages",
+ raw_request_body: { model: "claude-opus-4-8", max_tokens: 42 },
+ raw_request_headers: { "x-api-key": "redacted" },
+ });
+
+ render();
+
+ const textarea = getRequestTextarea();
+ await user.clear(textarea);
+ await user.type(textarea, '{{"model": "claude-opus-4-8"}');
+
+ await user.click(getTransformButton());
+
+ await waitFor(() => expect(transformRequestCallMock).toHaveBeenCalledTimes(1));
+ expect(transformRequestCallMock).toHaveBeenCalledWith(ACCESS_TOKEN, {
+ call_type: "completion",
+ request_body: { model: "claude-opus-4-8" },
+ });
+
+ const output = await screen.findByText(/api\.anthropic\.com\/v1\/messages/);
+ expect(output.textContent).toContain("curl -X POST");
+ expect(output.textContent).toContain("-H 'x-api-key: redacted'");
+ expect(output.textContent).toContain('"model": "claude-opus-4-8"');
+ expect(output.textContent).toContain('"max_tokens": 42');
+ expect(notify.success).toHaveBeenCalledWith("Request transformed successfully");
+ });
+
+ it("transforms on Cmd/Ctrl + Enter without clicking the button", async () => {
+ const user = userEvent.setup();
+ transformRequestCallMock.mockResolvedValue({
+ raw_request_api_base: "https://api.openai.com/v1/chat/completions",
+ raw_request_body: { model: "gpt-4o" },
+ raw_request_headers: {},
+ });
+
+ render();
+
+ getRequestTextarea().focus();
+ await user.keyboard("{Meta>}{Enter}{/Meta}");
+
+ await waitFor(() => expect(transformRequestCallMock).toHaveBeenCalledTimes(1));
+ });
+
+ it("rejects invalid JSON without calling the backend", async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ const textarea = getRequestTextarea();
+ await user.clear(textarea);
+ await user.type(textarea, "not json");
+ await user.click(getTransformButton());
+
+ await waitFor(() => expect(notify.fromBackend).toHaveBeenCalledWith("Invalid JSON in request body"));
+ expect(transformRequestCallMock).not.toHaveBeenCalled();
+ });
+
+ it("does not call the backend when there is no access token", async () => {
+ const user = userEvent.setup();
+
+ render();
+
+ await user.click(getTransformButton());
+
+ await waitFor(() => expect(notify.fromBackend).toHaveBeenCalledWith("No access token found"));
+ expect(transformRequestCallMock).not.toHaveBeenCalled();
+ });
+
+ it("reports a failed transform and leaves the placeholder curl in place", async () => {
+ const user = userEvent.setup();
+ vi.spyOn(console, "error").mockImplementation(() => {});
+ transformRequestCallMock.mockRejectedValue(new Error("boom"));
+
+ render();
+
+ await user.click(getTransformButton());
+
+ await waitFor(() => expect(notify.fromBackend).toHaveBeenCalledWith("Failed to transform request"));
+ expect(screen.getByText(/https:\/\/api\.openai\.com\/v1\/chat\/completions/)).toBeInTheDocument();
+ });
+
+ it("copies the transformed request to the clipboard", async () => {
+ const user = userEvent.setup();
+ const writeText = vi.spyOn(navigator.clipboard, "writeText");
+ transformRequestCallMock.mockResolvedValue({
+ raw_request_api_base: "https://api.anthropic.com/v1/messages",
+ raw_request_body: { model: "claude-opus-4-8" },
+ raw_request_headers: {},
+ });
+
+ render();
+
+ await user.click(getTransformButton());
+ await screen.findByText(/api\.anthropic\.com\/v1\/messages/);
+
+ await user.click(getCopyButton());
+
+ expect(writeText).toHaveBeenCalledTimes(1);
+ expect(writeText.mock.calls[0]?.[0]).toContain("https://api.anthropic.com/v1/messages");
+ expect(notify.success).toHaveBeenCalledWith("Copied to clipboard");
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx
index 04d1701de3f..0c41547b9b7 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/transform-request/TransformRequestPanel.tsx
@@ -1,9 +1,12 @@
import React, { useState } from "react";
-import { Button } from "antd";
-import { CopyOutlined } from "@ant-design/icons";
-import { Title } from "@tremor/react";
+import { ArrowRight, Copy } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { Textarea } from "@/components/ui/textarea";
+import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner";
import { transformRequestCall } from "@/components/networking";
import NotificationsManager from "@/components/molecules/notifications_manager";
+
interface TransformRequestPanelProps {
accessToken: string | null;
}
@@ -128,130 +131,50 @@ ${formattedBody}
};
return (
-
-
Playground
-
See how LiteLLM transforms your request for the specified provider.
-
+
+
Playground
+
+ See how LiteLLM transforms your request for the specified provider.
+
+
{/* Original Request Panel */}
-
-
-
Original Request
-
- The request you would send to LiteLLM /chat/completions endpoint.
-
-
+
+
+ Original Request
+ The request you would send to LiteLLM /chat/completions endpoint.
+
-
+
+
{/* Transformed Request Panel */}
-
-
-
Transformed Request
-
How LiteLLM transforms your request for the specified provider.
-
-
- Note: Sensitive headers are not shown.
-
-
+
+
+ Transformed Request
+ How LiteLLM transforms your request for the specified provider.
+ Note: Sensitive headers are not shown.
+
-
-
- {transformedResponse ||
- `curl -X POST \\
+
+
+
+ {transformedResponse ||
+ `curl -X POST \\
https://api.openai.com/v1/chat/completions \\
-H 'Authorization: Bearer sk-xxx' \\
-H 'Content-Type: application/json' \\
@@ -265,29 +188,33 @@ ${formattedBody}
],
"temperature": 0.7
}'`}
-
+
-
}
- style={{
- position: "absolute",
- right: "8px",
- top: "8px",
- }}
- size="small"
- onClick={() => {
- navigator.clipboard.writeText(transformedResponse || "");
- NotificationsManager.success("Copied to clipboard");
- }}
- />
-
-
+
+
+
+
-
-
+
+
Found an error? File an issue{" "}
-
+
here
.