feat(ui): give prompt compression its own top-level page

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-07-31 00:33:13 +00:00
parent c3da12161b
commit 66cc6e1ca2
13 changed files with 195 additions and 65 deletions

View file

@ -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

View file

@ -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(<CompressionView accessToken="test-token" />);
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(<CompressionView accessToken="test-token" />);
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(<CompressionView accessToken="test-token" />);
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(<CompressionView accessToken={null} />);
await waitFor(() => expect(screen.getByRole("button", { name: "Add guardrail" })).toBeInTheDocument());
expect(mockGetGuardrailsList).not.toHaveBeenCalled();
});
});

View file

@ -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<PromptCompressionTabProps> = ({ accessToken }) => {
const [form] = Form.useForm<CompressionFormValues>();
const EMPTY_FORM: CompressionFormValues = { name: "", apiBase: "", defaultOn: true };
const CompressionView: React.FC<CompressionViewProps> = ({ accessToken }) => {
const [formValues, setFormValues] = useState<CompressionFormValues>(EMPTY_FORM);
const [guardrails, setGuardrails] = useState<GuardrailListItem[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isSaving, setIsSaving] = useState<boolean>(false);
@ -46,22 +52,16 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
loadGuardrails();
}, [loadGuardrails]);
const handleAdd = async (values: CompressionFormValues) => {
const handleAdd = async (event: React.FormEvent<HTMLFormElement>) => {
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<PromptCompressionTabProps> = ({ accessToken
};
return (
<div className="w-full space-y-6">
<div className="w-full space-y-6 p-6">
<div>
<div className="flex items-center gap-2">
<Shrink className="size-6 text-emerald-600" strokeWidth={1.75} />
<h1 className="text-xl font-semibold text-foreground">Compression</h1>
</div>
<p className="mt-1 text-sm text-muted-foreground">
Configure prompt compression so you pay for fewer input tokens
</p>
</div>
<Card>
<CardHeader>
<CardTitle>Headroom prompt compression</CardTitle>
@ -80,7 +90,8 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
<CardContent>
<p className="mb-4 text-sm text-muted-foreground">
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.{" "}
<a
href="https://docs.litellm.ai/docs/proxy/headroom"
target="_blank"
@ -125,29 +136,39 @@ const PromptCompressionTab: React.FC<PromptCompressionTabProps> = ({ accessToken
<CardTitle>Add Headroom compression guardrail</CardTitle>
</CardHeader>
<CardContent>
<Form
form={form}
layout="vertical"
requiredMark={false}
onFinish={handleAdd}
initialValues={{ defaultOn: true }}
>
<Form.Item name="name" label="Name" rules={[{ required: true, message: "Name is required" }]}>
<Input placeholder="headroom-compression" />
</Form.Item>
<Form.Item
name="apiBase"
label="Headroom API base"
tooltip="Base URL of your Headroom compression service (LiteLLM calls its /v1/compress endpoint)"
extra="The URL where your Headroom compression service is hosted"
rules={[{ required: true, message: "API base is required" }]}
>
<Input placeholder="https://your-headroom-endpoint" />
</Form.Item>
<Form.Item name="defaultOn" label="Apply to all requests" valuePropName="checked">
<Switch />
</Form.Item>
<div className="mb-4 rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<form onSubmit={handleAdd} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="compression-name">Name</Label>
<Input
id="compression-name"
required
placeholder="headroom-compression"
value={formValues.name}
onChange={(event) => setFormValues({ ...formValues, name: event.target.value })}
/>
</div>
<div className="space-y-2">
<Label htmlFor="compression-api-base">Headroom API base</Label>
<Input
id="compression-api-base"
required
placeholder="https://your-headroom-endpoint"
value={formValues.apiBase}
onChange={(event) => setFormValues({ ...formValues, apiBase: event.target.value })}
/>
<p className="text-xs text-muted-foreground">
Base URL of your Headroom compression service; LiteLLM calls its /v1/compress endpoint
</p>
</div>
<Label htmlFor="compression-default-on">
<Switch
id="compression-default-on"
checked={formValues.defaultOn}
onCheckedChange={(checked) => setFormValues({ ...formValues, defaultOn: checked })}
/>
Apply to all requests
</Label>
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-3">
<p className="text-sm text-yellow-800">
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<PromptCompressionTabProps> = ({ accessToken
</p>
</div>
<div className="flex justify-end">
<Button type="primary" htmlType="submit" loading={isSaving}>
Add guardrail
<Button type="submit" disabled={isSaving}>
{isSaving ? "Adding..." : "Add guardrail"}
</Button>
</div>
</Form>
</form>
</CardContent>
</Card>
</div>
);
};
export default PromptCompressionTab;
export default CompressionView;

View file

@ -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 <CompressionView accessToken={accessToken} />;
}

View file

@ -26,8 +26,6 @@ vi.mock("@/app/(dashboard)/router-settings/_components/general_settings", () =>
PromptCachingPanel: () => <div data-testid="caching-settings" />,
}));
vi.mock("./PromptCompressionTab", () => ({ __esModule: true, default: () => <div /> }));
import CostOptimizationView from "./CostOptimizationView";
const singlePage = {

View file

@ -2,7 +2,6 @@ import { fireEvent, render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
vi.mock("./UsageTab", () => ({ __esModule: true, default: () => <div data-testid="usage-tab" /> }));
vi.mock("./PromptCompressionTab", () => ({ __esModule: true, default: () => <div data-testid="compression-tab" /> }));
vi.mock("./PromptCachingTab", () => ({ __esModule: true, default: () => <div data-testid="caching-tab" /> }));
import CostOptimizationView from "./CostOptimizationView";
@ -10,12 +9,12 @@ import CostOptimizationView from "./CostOptimizationView";
const renderView = () => render(<CostOptimizationView accessToken="test-token" userId="u1" userRole="proxy_admin" />);
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");
});
});

View file

@ -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<CostOptimizationViewProps> = ({ accessToken
label: "Usage",
children: <UsageTab accessToken={accessToken} activity={activity} />,
},
{
key: "compression",
label: "Prompt Compression",
children: <PromptCompressionTab accessToken={accessToken} />,
},
{
key: "caching",
label: "Prompt Caching",
@ -44,8 +38,8 @@ const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken
<h1 className="text-xl font-semibold text-foreground">Cost Optimization</h1>
</div>
<p className="mt-1 text-sm text-muted-foreground">
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
</p>
</div>

View file

@ -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" });
});

View file

@ -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: <Server {...ICON} /> },
{ key: "skills", page: "skills", label: "Skills", icon: <Blocks {...ICON} />, roles: all_admin_roles },
{ key: "guardrails", page: "guardrails", label: "Guardrails", icon: <Shield {...ICON} /> },
{
key: "compression",
page: "compression",
label: "Compression",
icon: <Shrink {...ICON} />,
roles: all_admin_roles,
},
{
key: "policies",
page: "policies",

View file

@ -19,7 +19,8 @@ export const pageDescriptions: Record<string, string> = {
"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",

View file

@ -45,6 +45,7 @@ export const MIGRATED_PAGES: Record<string, string> = {
new_usage: "usage",
usage: "old-usage",
"cost-optimization": "cost-optimization",
compression: "compression",
agents: "agents",
"router-settings": "router-settings",
users: "users",