From 66cc6e1ca22cb56d63d1d21660c41a0f058b6685 Mon Sep 17 00:00:00 2001 From: yassin Date: Fri, 31 Jul 2026 00:33:13 +0000 Subject: [PATCH] feat(ui): give prompt compression its own top-level page Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ui/litellm-dashboard/eslint-suppressions.json | 5 - .../_components/CompressionView.test.tsx | 99 ++++++++++++++++ .../_components/CompressionView.tsx} | 107 +++++++++++------- .../_components/helpers.test.ts | 0 .../_components/helpers.ts | 0 .../src/app/(dashboard)/compression/page.tsx | 9 ++ .../CostOptimizationView.activity.test.tsx | 2 - .../_components/CostOptimizationView.test.tsx | 11 +- .../_components/CostOptimizationView.tsx | 10 +- .../src/components/leftnav.test.tsx | 5 + .../src/components/leftnav.tsx | 8 ++ .../src/components/page_metadata.ts | 3 +- .../src/utils/migratedPages.ts | 1 + 13 files changed, 195 insertions(+), 65 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.test.tsx rename ui/litellm-dashboard/src/app/(dashboard)/{cost-optimization/_components/PromptCompressionTab.tsx => compression/_components/CompressionView.tsx} (61%) rename ui/litellm-dashboard/src/app/(dashboard)/{cost-optimization => compression}/_components/helpers.test.ts (100%) rename ui/litellm-dashboard/src/app/(dashboard)/{cost-optimization => compression}/_components/helpers.ts (100%) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/compression/page.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 6819b2851f5..ffe902474c1 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -212,11 +212,6 @@ "count": 1 } }, - "src/app/(dashboard)/cost-optimization/_components/PromptCompressionTab.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "src/app/(dashboard)/cost-tracking/_components/add_margin_form.tsx": { "local/filename-pascal-case": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.test.tsx new file mode 100644 index 00000000000..8a618607543 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.test.tsx @@ -0,0 +1,99 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockCreateGuardrailCall, mockGetGuardrailsList } = vi.hoisted(() => ({ + mockCreateGuardrailCall: vi.fn(), + mockGetGuardrailsList: vi.fn(), +})); + +vi.mock("@/components/networking", () => ({ + createGuardrailCall: mockCreateGuardrailCall, + getGuardrailsList: mockGetGuardrailsList, +})); + +vi.mock("@/components/molecules/notifications_manager", () => ({ + default: { success: vi.fn(), fromBackend: vi.fn() }, +})); + +import CompressionView from "./CompressionView"; + +const headroomGuardrail = { + guardrail_id: "g-1", + guardrail_name: "headroom-prod", + litellm_params: { guardrail: "headroom", api_base: "https://headroom.internal", default_on: true }, +}; + +const piiGuardrail = { + guardrail_id: "g-2", + guardrail_name: "presidio-pii", + litellm_params: { guardrail: "presidio", api_base: "https://presidio.internal", default_on: false }, +}; + +describe("CompressionView", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockGetGuardrailsList.mockResolvedValue({ guardrails: [headroomGuardrail, piiGuardrail] }); + mockCreateGuardrailCall.mockResolvedValue({}); + }); + + it("lists only compression guardrails with their always-on state", async () => { + render(); + + await waitFor(() => expect(screen.getByText("headroom-prod")).toBeInTheDocument()); + expect(screen.getByText("https://headroom.internal")).toBeInTheDocument(); + expect(screen.getByText("Always on")).toBeInTheDocument(); + expect(screen.queryByText("presidio-pii")).not.toBeInTheDocument(); + }); + + it("creates a headroom guardrail from the form and reloads the list", async () => { + render(); + await waitFor(() => expect(mockGetGuardrailsList).toHaveBeenCalledTimes(1)); + + fireEvent.change(screen.getByPlaceholderText("headroom-compression"), { target: { value: "headroom-new" } }); + fireEvent.change(screen.getByPlaceholderText("https://your-headroom-endpoint"), { + target: { value: "https://new-headroom.internal" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Add guardrail" })); + + await waitFor(() => + expect(mockCreateGuardrailCall).toHaveBeenCalledWith("test-token", { + guardrail_name: "headroom-new", + litellm_params: { + guardrail: "headroom", + mode: "pre_call", + api_base: "https://new-headroom.internal", + default_on: true, + }, + }), + ); + await waitFor(() => expect(mockGetGuardrailsList).toHaveBeenCalledTimes(2)); + }); + + it("sends default_on false when 'Apply to all requests' is switched off", async () => { + render(); + await waitFor(() => expect(mockGetGuardrailsList).toHaveBeenCalledTimes(1)); + + fireEvent.change(screen.getByPlaceholderText("headroom-compression"), { target: { value: "headroom-optin" } }); + fireEvent.change(screen.getByPlaceholderText("https://your-headroom-endpoint"), { + target: { value: "https://optin-headroom.internal" }, + }); + fireEvent.click(screen.getByRole("switch")); + fireEvent.click(screen.getByRole("button", { name: "Add guardrail" })); + + await waitFor(() => + expect(mockCreateGuardrailCall).toHaveBeenCalledWith( + "test-token", + expect.objectContaining({ + litellm_params: expect.objectContaining({ default_on: false }), + }), + ), + ); + }); + + it("does not call the backend without an access token", async () => { + render(); + + await waitFor(() => expect(screen.getByRole("button", { name: "Add guardrail" })).toBeInTheDocument()); + expect(mockGetGuardrailsList).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/PromptCompressionTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.tsx similarity index 61% rename from ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/PromptCompressionTab.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.tsx index 071ad1d6bd5..2bbc8136567 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/PromptCompressionTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/CompressionView.tsx @@ -1,9 +1,13 @@ "use client"; import React, { useCallback, useEffect, useState } from "react"; -import { Button, Form, Input, Switch } from "antd"; +import { Shrink } from "lucide-react"; +import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; import { createGuardrailCall, getGuardrailsList } from "@/components/networking"; import NotificationsManager from "@/components/molecules/notifications_manager"; import { @@ -13,7 +17,7 @@ import { GuardrailListResponse, } from "./helpers"; -interface PromptCompressionTabProps { +interface CompressionViewProps { accessToken: string | null; } @@ -23,8 +27,10 @@ interface CompressionFormValues { defaultOn: boolean; } -const PromptCompressionTab: React.FC = ({ accessToken }) => { - const [form] = Form.useForm(); +const EMPTY_FORM: CompressionFormValues = { name: "", apiBase: "", defaultOn: true }; + +const CompressionView: React.FC = ({ accessToken }) => { + const [formValues, setFormValues] = useState(EMPTY_FORM); const [guardrails, setGuardrails] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); @@ -46,22 +52,16 @@ const PromptCompressionTab: React.FC = ({ accessToken loadGuardrails(); }, [loadGuardrails]); - const handleAdd = async (values: CompressionFormValues) => { + const handleAdd = async (event: React.FormEvent) => { + event.preventDefault(); if (!accessToken) { return; } setIsSaving(true); try { - await createGuardrailCall( - accessToken, - buildCompressionGuardrailPayload({ - name: values.name, - apiBase: values.apiBase, - defaultOn: values.defaultOn ?? true, - }), - ); + await createGuardrailCall(accessToken, buildCompressionGuardrailPayload(formValues)); NotificationsManager.success("Compression guardrail created"); - form.resetFields(); + setFormValues(EMPTY_FORM); await loadGuardrails(); } catch (error) { console.error("Failed to create compression guardrail:", error); @@ -72,7 +72,17 @@ const PromptCompressionTab: React.FC = ({ accessToken }; return ( -
+
+
+
+ +

Compression

+
+

+ Configure prompt compression so you pay for fewer input tokens +

+
+ Headroom prompt compression @@ -80,7 +90,8 @@ const PromptCompressionTab: React.FC = ({ accessToken

Headroom is a native LiteLLM guardrail that compresses your prompts before they reach the model, so you pay - for fewer input tokens. The tokens it removes are priced and shown on the Usage tab as compression savings.{" "} + for fewer input tokens. The tokens it removes are priced and shown as compression savings on the Cost + Optimization dashboard.{" "} = ({ accessToken Add Headroom compression guardrail -

- - - - - - - - - -
+ +
+ + setFormValues({ ...formValues, name: event.target.value })} + /> +
+
+ + setFormValues({ ...formValues, apiBase: event.target.value })} + /> +

+ Base URL of your Headroom compression service; LiteLLM calls its /v1/compress endpoint +

+
+ +

Applying compression to all requests is available to all users. Enabling it selectively per key or team is a LiteLLM Enterprise feature. Get a trial key{" "} @@ -162,15 +183,15 @@ const PromptCompressionTab: React.FC = ({ accessToken

-
- +
); }; -export default PromptCompressionTab; +export default CompressionView; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/helpers.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/helpers.test.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/helpers.test.ts rename to ui/litellm-dashboard/src/app/(dashboard)/compression/_components/helpers.test.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/helpers.ts b/ui/litellm-dashboard/src/app/(dashboard)/compression/_components/helpers.ts similarity index 100% rename from ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/helpers.ts rename to ui/litellm-dashboard/src/app/(dashboard)/compression/_components/helpers.ts diff --git a/ui/litellm-dashboard/src/app/(dashboard)/compression/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/compression/page.tsx new file mode 100644 index 00000000000..3f4feddf674 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/compression/page.tsx @@ -0,0 +1,9 @@ +"use client"; + +import CompressionView from "./_components/CompressionView"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; + +export default function CompressionPage() { + const { accessToken } = useAuthorized(); + return ; +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx index ca7adf07941..176ccbd05ea 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.activity.test.tsx @@ -26,8 +26,6 @@ vi.mock("@/app/(dashboard)/router-settings/_components/general_settings", () => PromptCachingPanel: () =>
, })); -vi.mock("./PromptCompressionTab", () => ({ __esModule: true, default: () =>
})); - import CostOptimizationView from "./CostOptimizationView"; const singlePage = { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx index 42dc7719144..c8d8d6e16f9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.test.tsx @@ -2,7 +2,6 @@ import { fireEvent, render } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; vi.mock("./UsageTab", () => ({ __esModule: true, default: () =>
})); -vi.mock("./PromptCompressionTab", () => ({ __esModule: true, default: () =>
})); vi.mock("./PromptCachingTab", () => ({ __esModule: true, default: () =>
})); import CostOptimizationView from "./CostOptimizationView"; @@ -10,12 +9,12 @@ import CostOptimizationView from "./CostOptimizationView"; const renderView = () => render(); describe("CostOptimizationView", () => { - it("renders the three cost-optimization tabs and no autorouter tab", () => { + it("renders the reporting tabs, with compression config moved to its own page", () => { const { getByText, queryByText } = renderView(); expect(getByText("Usage")).toBeInTheDocument(); - expect(getByText("Prompt Compression")).toBeInTheDocument(); expect(getByText("Prompt Caching")).toBeInTheDocument(); + expect(queryByText("Prompt Compression")).not.toBeInTheDocument(); expect(queryByText("Autorouter")).not.toBeInTheDocument(); }); @@ -23,11 +22,11 @@ describe("CostOptimizationView", () => { const { getByRole } = renderView(); expect(getByRole("tab", { name: "Usage" })).toHaveAttribute("aria-selected", "true"); - expect(getByRole("tab", { name: "Prompt Compression" })).toHaveAttribute("aria-selected", "false"); + expect(getByRole("tab", { name: "Prompt Caching" })).toHaveAttribute("aria-selected", "false"); - fireEvent.click(getByRole("tab", { name: "Prompt Compression" })); + fireEvent.click(getByRole("tab", { name: "Prompt Caching" })); expect(getByRole("tab", { name: "Usage" })).toHaveAttribute("aria-selected", "false"); - expect(getByRole("tab", { name: "Prompt Compression" })).toHaveAttribute("aria-selected", "true"); + expect(getByRole("tab", { name: "Prompt Caching" })).toHaveAttribute("aria-selected", "true"); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx index f6593e80999..8eeee999bfb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx @@ -5,7 +5,6 @@ import { PiggyBank } from "lucide-react"; import { Alert, Tabs } from "antd"; import UsageTab from "./UsageTab"; -import PromptCompressionTab from "./PromptCompressionTab"; import PromptCachingTab from "./PromptCachingTab"; import { useDailyActivityRange } from "./useDailyActivityRange"; @@ -24,11 +23,6 @@ const CostOptimizationView: React.FC = ({ accessToken label: "Usage", children: , }, - { - key: "compression", - label: "Prompt Compression", - children: , - }, { key: "caching", label: "Prompt Caching", @@ -44,8 +38,8 @@ const CostOptimizationView: React.FC = ({ accessToken

Cost Optimization

- Track and configure the mechanisms that save you money: prompt compression and prompt caching. Auto routers - live under Models + Endpoints, on the Auto-Routers tab + Track what the money-saving mechanisms are actually saving you. Prompt compression is configured under AI + Gateway, on the Compression page; auto routers live under Models + Endpoints, on the Auto-Routers tab

diff --git a/ui/litellm-dashboard/src/components/leftnav.test.tsx b/ui/litellm-dashboard/src/components/leftnav.test.tsx index dc893643559..c6512f81170 100644 --- a/ui/litellm-dashboard/src/components/leftnav.test.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.test.tsx @@ -106,6 +106,7 @@ describe("Sidebar (leftnav)", () => { "Agentic", "MCP Servers", "Guardrails", + "Compression", "Policies", "Tools", "Usage", @@ -286,6 +287,10 @@ describe("getBreadcrumb", () => { expect(getBreadcrumb("logs")).toEqual({ section: "Observability", title: "Logs" }); }); + it("resolves compression as a top-level AI Gateway page", () => { + expect(getBreadcrumb("compression")).toEqual({ section: "AI Gateway", title: "Compression" }); + }); + it("resolves a nested child page to its parent section", () => { expect(getBreadcrumb("search-tools")).toEqual({ section: "AI Gateway", title: "Search Tools" }); }); diff --git a/ui/litellm-dashboard/src/components/leftnav.tsx b/ui/litellm-dashboard/src/components/leftnav.tsx index af76fccf9eb..b91b7d9f133 100644 --- a/ui/litellm-dashboard/src/components/leftnav.tsx +++ b/ui/litellm-dashboard/src/components/leftnav.tsx @@ -53,6 +53,7 @@ import { Settings as SettingsIcon, Shield, ShieldCheck, + Shrink, Tags, Terminal, User, @@ -152,6 +153,13 @@ const menuGroups: MenuGroup[] = [ { key: "mcp-servers", page: "mcp-servers", label: "MCP Servers", icon: }, { key: "skills", page: "skills", label: "Skills", icon: , roles: all_admin_roles }, { key: "guardrails", page: "guardrails", label: "Guardrails", icon: }, + { + key: "compression", + page: "compression", + label: "Compression", + icon: , + roles: all_admin_roles, + }, { key: "policies", page: "policies", diff --git a/ui/litellm-dashboard/src/components/page_metadata.ts b/ui/litellm-dashboard/src/components/page_metadata.ts index 0f2ff639bb3..0c950ca4bab 100644 --- a/ui/litellm-dashboard/src/components/page_metadata.ts +++ b/ui/litellm-dashboard/src/components/page_metadata.ts @@ -19,7 +19,8 @@ export const pageDescriptions: Record = { "tool-policies": "Configure tool use policies and permissions", "vector-stores": "Manage vector databases for embeddings", new_usage: "View usage analytics and metrics", - "cost-optimization": "Track and configure cost-saving features: prompt compression, caching, and auto routing", + "cost-optimization": "Track savings from cost-saving features: prompt compression, caching, and auto routing", + compression: "Configure prompt compression so requests are billed for fewer input tokens", logs: "Access request and response logs", "guardrails-monitor": "Monitor guardrail performance and view logs", users: "Manage internal user accounts and permissions", diff --git a/ui/litellm-dashboard/src/utils/migratedPages.ts b/ui/litellm-dashboard/src/utils/migratedPages.ts index 73ab71ce4ac..4dcc82c77dc 100644 --- a/ui/litellm-dashboard/src/utils/migratedPages.ts +++ b/ui/litellm-dashboard/src/utils/migratedPages.ts @@ -45,6 +45,7 @@ export const MIGRATED_PAGES: Record = { new_usage: "usage", usage: "old-usage", "cost-optimization": "cost-optimization", + compression: "compression", agents: "agents", "router-settings": "router-settings", users: "users",