diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json
index 289012659a1..09d0032e6c9 100644
--- a/ui/litellm-dashboard/eslint-suppressions.json
+++ b/ui/litellm-dashboard/eslint-suppressions.json
@@ -2959,11 +2959,6 @@
"count": 1
}
},
- "src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.tsx": {
- "no-restricted-imports": {
- "count": 1
- }
- },
"src/components/common_components/budget_duration_dropdown.tsx": {
"local/filename-pascal-case": {
"count": 1
@@ -3549,11 +3544,6 @@
"count": 1
}
},
- "src/components/routing_groups/RoutingGroupsTable.tsx": {
- "no-restricted-imports": {
- "count": 2
- }
- },
"src/components/routing_groups/index.tsx": {
"local/filename-pascal-case": {
"count": 1
diff --git a/ui/litellm-dashboard/src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.test.tsx b/ui/litellm-dashboard/src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.test.tsx
deleted file mode 100644
index 58395371bbe..00000000000
--- a/ui/litellm-dashboard/src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.test.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { render, screen, waitFor } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { describe, expect, it, vi } from "vitest";
-import { TableHeaderSortDropdown } from "./TableHeaderSortDropdown";
-
-describe("TableHeaderSortDropdown", () => {
- it("should render", () => {
- const onSortChange = vi.fn();
- render( );
- expect(screen.getByRole("button")).toBeInTheDocument();
- });
-
- it("should open dropdown menu when button is clicked", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- expect(screen.getByText("Ascending")).toBeInTheDocument();
- expect(screen.getByText("Descending")).toBeInTheDocument();
- expect(screen.getByText("Reset")).toBeInTheDocument();
- });
- });
-
- it("should call onSortChange with asc when ascending option is clicked", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- expect(screen.getByText("Ascending")).toBeInTheDocument();
- });
-
- const ascendingOption = screen.getByText("Ascending");
- await user.click(ascendingOption);
-
- expect(onSortChange).toHaveBeenCalledTimes(1);
- expect(onSortChange).toHaveBeenCalledWith("asc");
- });
-
- it("should call onSortChange with desc when descending option is clicked", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- expect(screen.getByText("Descending")).toBeInTheDocument();
- });
-
- const descendingOption = screen.getByText("Descending");
- await user.click(descendingOption);
-
- expect(onSortChange).toHaveBeenCalledTimes(1);
- expect(onSortChange).toHaveBeenCalledWith("desc");
- });
-
- it("should call onSortChange with false when reset option is clicked", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- expect(screen.getByText("Reset")).toBeInTheDocument();
- });
-
- const resetOption = screen.getByText("Reset");
- await user.click(resetOption);
-
- expect(onSortChange).toHaveBeenCalledTimes(1);
- expect(onSortChange).toHaveBeenCalledWith(false);
- });
-
- it("should highlight ascending option when sort state is asc", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- const ascendingOption = screen.getByText("Ascending");
- const menuItem = ascendingOption.closest(".ant-dropdown-menu-item");
- expect(menuItem).toHaveClass("ant-dropdown-menu-item-selected");
- });
- });
-
- it("should highlight descending option when sort state is desc", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- const descendingOption = screen.getByText("Descending");
- const menuItem = descendingOption.closest(".ant-dropdown-menu-item");
- expect(menuItem).toHaveClass("ant-dropdown-menu-item-selected");
- });
- });
-
- it("should not highlight any option when sort state is false", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- render( );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- await waitFor(() => {
- expect(screen.getByText("Ascending")).toBeInTheDocument();
- });
-
- const ascendingOption = screen.getByText("Ascending");
- const menuItem = ascendingOption.closest(".ant-dropdown-menu-item");
- expect(menuItem).not.toHaveClass("ant-dropdown-menu-item-selected");
- });
-
- it("should stop event propagation when button is clicked", async () => {
- const user = userEvent.setup();
- const onSortChange = vi.fn();
- const onParentClick = vi.fn();
-
- render(
-
,
- );
-
- const button = screen.getByRole("button");
- await user.click(button);
-
- expect(onParentClick).not.toHaveBeenCalled();
- });
-});
diff --git a/ui/litellm-dashboard/src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.tsx b/ui/litellm-dashboard/src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.tsx
deleted file mode 100644
index c83257c5c83..00000000000
--- a/ui/litellm-dashboard/src/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import React from "react";
-import { Button, Dropdown, MenuProps } from "antd";
-import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, XIcon } from "@heroicons/react/outline";
-
-export type SortState = "asc" | "desc" | false;
-
-interface TableHeaderSortDropdownProps {
- /**
- * Current sort state: "asc", "desc", or false for neutral
- */
- sortState: SortState;
- /**
- * Callback when sort state changes
- * @param newState - The new sort state: "asc", "desc", or false
- */
- onSortChange: (newState: SortState) => void;
- /**
- * Optional column ID for identification
- */
- columnId?: string;
-}
-
-export const TableHeaderSortDropdown: React.FC = ({ sortState, onSortChange }) => {
- const handleMenuClick: MenuProps["onClick"] = ({ key }) => {
- if (key === "asc") {
- onSortChange("asc");
- } else if (key === "desc") {
- onSortChange("desc");
- } else if (key === "reset") {
- onSortChange(false);
- }
- };
-
- const menuItems: MenuProps["items"] = [
- {
- key: "asc",
- label: "Ascending",
- icon: ,
- },
- {
- key: "desc",
- label: "Descending",
- icon: ,
- },
- {
- key: "reset",
- label: "Reset",
- icon: ,
- },
- ];
-
- // Determine which icon to display based on current sort state
- const renderIcon = () => {
- if (sortState === "asc") {
- return ;
- } else if (sortState === "desc") {
- return ;
- } else {
- return ;
- }
- };
-
- return (
-
- e.stopPropagation()}
- icon={renderIcon()}
- className={sortState ? "text-blue-500 hover:text-blue-600" : "text-gray-400 hover:text-blue-500"}
- />
-
- );
-};
diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupUsagePanel.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupUsagePanel.tsx
new file mode 100644
index 00000000000..fafea569012
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupUsagePanel.tsx
@@ -0,0 +1,91 @@
+"use client";
+
+import { Code2 } from "lucide-react";
+import React from "react";
+
+import CodeBlock from "@/components/CodeBlock";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+
+import { formatStrategyLabel } from "./strategy";
+import type { RoutingGroup } from "./types";
+
+interface RoutingGroupUsagePanelProps {
+ group: RoutingGroup;
+ baseUrl: string;
+}
+
+const exampleModel = (group: RoutingGroup): string => group.models[0] ?? "";
+
+const buildCurlSnippet = (group: RoutingGroup, baseUrl: string): string =>
+ `curl -X POST '${baseUrl}/v1/chat/completions' \\
+ -H 'Content-Type: application/json' \\
+ -H 'Authorization: Bearer $LITELLM_API_KEY' \\
+ -d '{
+ "model": "${exampleModel(group)}",
+ "messages": [{"role": "user", "content": "Hello!"}]
+ }'`;
+
+const buildPythonSnippet = (group: RoutingGroup, baseUrl: string): string =>
+ `from openai import OpenAI
+
+client = OpenAI(
+ api_key="$LITELLM_API_KEY",
+ base_url="${baseUrl}",
+)
+
+response = client.chat.completions.create(
+ model="${exampleModel(group)}",
+ messages=[{"role": "user", "content": "Hello!"}],
+)
+
+print(response)`;
+
+const buildJsSnippet = (group: RoutingGroup, baseUrl: string): string =>
+ `import OpenAI from "openai";
+
+const client = new OpenAI({
+ apiKey: process.env.LITELLM_API_KEY,
+ baseURL: "${baseUrl}",
+});
+
+const response = await client.chat.completions.create({
+ model: "${exampleModel(group)}",
+ messages: [{ role: "user", content: "Hello!" }],
+});
+
+console.log(response);`;
+
+const SNIPPET_TABS = [
+ { value: "curl", label: "cURL", language: "bash", build: buildCurlSnippet },
+ { value: "python", label: "Python (OpenAI SDK)", language: "python", build: buildPythonSnippet },
+ { value: "javascript", label: "JavaScript (OpenAI SDK)", language: "javascript", build: buildJsSnippet },
+] as const;
+
+export function RoutingGroupUsagePanel({ group, baseUrl }: RoutingGroupUsagePanelProps) {
+ return (
+
+
+
+ How routing works for this group
+
+
+ Callers request any model in the group by name; LiteLLM picks a deployment behind the scenes using the{" "}
+ {formatStrategyLabel(group.routing_strategy)} strategy.
+
+
+
+ {SNIPPET_TABS.map((tab) => (
+
+ {tab.label}
+
+ ))}
+
+ {SNIPPET_TABS.map((tab) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.test.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.test.tsx
new file mode 100644
index 00000000000..6f14b76e2fd
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.test.tsx
@@ -0,0 +1,145 @@
+import { render, screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+import RoutingGroupsTable from "./RoutingGroupsTable";
+import type { RoutingGroup } from "./types";
+
+describe("RoutingGroupsTable", () => {
+ const onEdit = vi.fn();
+ const onDelete = vi.fn();
+
+ const prodGroup: RoutingGroup = {
+ group_name: "prod-group",
+ models: ["gpt-4o", "claude-sonnet-4-5"],
+ routing_strategy: "usage-based-routing",
+ };
+
+ const devGroup: RoutingGroup = {
+ group_name: "dev-group",
+ models: ["gpt-4o-mini"],
+ routing_strategy: "simple-shuffle",
+ };
+
+ const defaultProps = {
+ groups: [] as RoutingGroup[],
+ onEdit,
+ onDelete,
+ proxyBaseUrl: "https://proxy.example.com",
+ };
+
+ const rowFor = (groupName: string): HTMLElement => {
+ const row = document.querySelector(`[data-row-id="${groupName}"]`);
+ if (!(row instanceof HTMLElement)) {
+ throw new Error(`No row rendered for ${groupName}`);
+ }
+ return row;
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should render every column header", () => {
+ render( );
+ for (const header of ["Group Name", "Models", "Strategy"]) {
+ expect(screen.getByText(header)).toBeInTheDocument();
+ }
+ });
+
+ it("should show the empty state when there are no groups", () => {
+ render( );
+ expect(screen.getByText("No routing groups yet")).toBeInTheDocument();
+ });
+
+ it("should render the group name, its models, and a human-readable strategy label", () => {
+ render( );
+ const row = rowFor("prod-group");
+ expect(within(row).getByText("prod-group")).toBeInTheDocument();
+ expect(within(row).getByText("gpt-4o")).toBeInTheDocument();
+ expect(within(row).getByText("claude-sonnet-4-5")).toBeInTheDocument();
+ expect(within(row).getByText("Usage Based")).toBeInTheDocument();
+ });
+
+ it("should fall back to the raw strategy value when it has no friendly label", () => {
+ render( );
+ expect(within(rowFor("prod-group")).getByText("custom-strategy")).toBeInTheDocument();
+ });
+
+ it("should collapse models beyond the first three behind a +N more badge", () => {
+ const wideGroup: RoutingGroup = { ...prodGroup, models: ["a", "b", "c", "d", "e"] };
+ render( );
+ const row = rowFor("prod-group");
+ expect(within(row).getByText("+2 more")).toBeInTheDocument();
+ expect(within(row).queryByText("d")).not.toBeInTheDocument();
+ });
+
+ it("should keep the incoming order until a column is sorted", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ const namesInOrder = () =>
+ screen
+ .getAllByRole("row")
+ .slice(1)
+ .map((row) => row.getAttribute("data-row-id"));
+
+ expect(namesInOrder()).toEqual(["prod-group", "dev-group"]);
+
+ await user.click(screen.getByTestId("sort-header-group_name"));
+ expect(namesInOrder()).toEqual(["dev-group", "prod-group"]);
+ });
+
+ it("should toggle the usage panel when the group name is clicked", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ expect(screen.queryByText("How routing works for this group")).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "prod-group" }));
+ expect(await screen.findByText("How routing works for this group")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "prod-group" }));
+ expect(screen.queryByText("How routing works for this group")).not.toBeInTheDocument();
+ });
+
+ it("should build the usage snippet from the proxy base url and the group's first model", async () => {
+ const user = userEvent.setup();
+ render( );
+ await user.click(screen.getByRole("button", { name: "prod-group" }));
+
+ const panel = (await screen.findByText("How routing works for this group")).closest("div")?.parentElement;
+ expect(panel?.textContent).toContain("https://proxy.example.com");
+ expect(panel?.textContent).toContain("gpt-4o");
+ });
+
+ it("should expand only the clicked group", async () => {
+ const user = userEvent.setup();
+ render( );
+
+ await user.click(screen.getByRole("button", { name: "dev-group" }));
+ expect(await screen.findAllByText("How routing works for this group")).toHaveLength(1);
+ expect(within(rowFor("prod-group")).queryByText("How routing works for this group")).not.toBeInTheDocument();
+ });
+
+ it("should edit a group through the actions menu", async () => {
+ const user = userEvent.setup();
+ render( );
+ await user.click(screen.getByTestId("routing-group-actions-prod-group"));
+ await user.click(await screen.findByTestId("routing-group-action-edit"));
+ expect(onEdit).toHaveBeenCalledWith(prodGroup);
+ });
+
+ it("should delete a group through the actions menu", async () => {
+ const user = userEvent.setup();
+ render( );
+ await user.click(screen.getByTestId("routing-group-actions-prod-group"));
+ await user.click(await screen.findByTestId("routing-group-action-delete"));
+ expect(onDelete).toHaveBeenCalledWith(prodGroup);
+ });
+
+ it("should show skeleton rows instead of the empty state while loading", () => {
+ render( );
+ expect(screen.queryByText("No routing groups yet")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx
index 96f6e578c83..fce887fc63b 100644
--- a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx
+++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTable.tsx
@@ -1,229 +1,82 @@
"use client";
-import React, { useState } from "react";
-import { Flex, Table, Tabs, Tag, Tooltip, Typography, Button } from "antd";
-import type { ColumnsType } from "antd/es/table";
-import { BranchesOutlined, DeleteOutlined, EditOutlined, CodeOutlined } from "@ant-design/icons";
-import type { RoutingGroup } from "./types";
+import type { ExpandedState, SortingState } from "@tanstack/react-table";
+import { Inbox } from "lucide-react";
+import React, { useCallback, useMemo, useState } from "react";
-const { Text, Paragraph } = Typography;
+import { DataTable } from "@/components/shared/DataTable";
+
+import { RoutingGroupUsagePanel } from "./RoutingGroupUsagePanel";
+import { getRoutingGroupsTableColumns } from "./RoutingGroupsTableColumns";
+import type { RoutingGroup } from "./types";
interface RoutingGroupsTableProps {
groups: RoutingGroup[];
- loading?: boolean;
+ isLoading?: boolean;
onEdit: (group: RoutingGroup) => void;
onDelete: (group: RoutingGroup) => void;
proxyBaseUrl?: string;
}
-const formatStrategyLabel = (strategy: string): string => {
- switch (strategy) {
- case "simple-shuffle":
- return "Simple Shuffle";
- case "least-busy":
- return "Least Busy";
- case "usage-based-routing":
- return "Usage Based";
- case "latency-based-routing":
- return "Latency Based";
- default:
- return strategy;
- }
-};
-
const resolveBaseUrl = (proxyBaseUrl?: string): string => {
if (proxyBaseUrl && proxyBaseUrl.trim()) return proxyBaseUrl;
if (typeof window !== "undefined" && window.location?.origin) return window.location.origin;
return "";
};
-const exampleModel = (group: RoutingGroup): string => group.models[0] ?? "";
-
-const buildCurlSnippet = (group: RoutingGroup, baseUrl: string): string =>
- `curl -X POST '${baseUrl}/v1/chat/completions' \\
- -H 'Content-Type: application/json' \\
- -H 'Authorization: Bearer $LITELLM_API_KEY' \\
- -d '{
- "model": "${exampleModel(group)}",
- "messages": [{"role": "user", "content": "Hello!"}]
- }'`;
-
-const buildPythonSnippet = (group: RoutingGroup, baseUrl: string): string =>
- `from openai import OpenAI
-
-client = OpenAI(
- api_key="$LITELLM_API_KEY",
- base_url="${baseUrl}",
-)
-
-response = client.chat.completions.create(
- model="${exampleModel(group)}",
- messages=[{"role": "user", "content": "Hello!"}],
-)
-
-print(response)`;
-
-const buildJsSnippet = (group: RoutingGroup, baseUrl: string): string =>
- `import OpenAI from "openai";
-
-const client = new OpenAI({
- apiKey: process.env.LITELLM_API_KEY,
- baseURL: "${baseUrl}",
-});
-
-const response = await client.chat.completions.create({
- model: "${exampleModel(group)}",
- messages: [{ role: "user", content: "Hello!" }],
-});
-
-console.log(response);`;
-
-interface RoutingGroupSnippetProps {
- group: RoutingGroup;
- baseUrl: string;
+function EmptyState() {
+ return (
+
+
+
+
+
No routing groups yet
+
+ Create a group to load-balance a set of models behind one name.
+
+
+ );
}
-const SNIPPET_BLOCK_STYLE: React.CSSProperties = {
- backgroundColor: "#111827",
- color: "#f3f4f6",
- borderRadius: 6,
- padding: 16,
- fontSize: 12,
- whiteSpace: "pre",
- overflowX: "auto",
-};
-
-const RoutingGroupSnippet: React.FC = ({ group, baseUrl }) => {
- const snippets = {
- curl: buildCurlSnippet(group, baseUrl),
- python: buildPythonSnippet(group, baseUrl),
- javascript: buildJsSnippet(group, baseUrl),
- } as const;
- type SnippetKey = keyof typeof snippets;
- const [activeKey, setActiveKey] = useState("curl");
-
- const items = [
- { key: "curl", label: "cURL" },
- { key: "python", label: "Python (OpenAI SDK)" },
- { key: "javascript", label: "JavaScript (OpenAI SDK)" },
- ].map(({ key, label }) => ({
- key,
- label,
- children: (
-
- {snippets[key as SnippetKey]}
-
- ),
- }));
-
- return (
- setActiveKey(k as SnippetKey)}
- items={items}
- tabBarExtraContent={
-
- }
- />
- );
-};
-
-const RoutingGroupsTable: React.FC = ({ groups, loading, onEdit, onDelete, proxyBaseUrl }) => {
- const [expandedRowKeys, setExpandedRowKeys] = useState([]);
+const RoutingGroupsTable: React.FC = ({
+ groups,
+ isLoading,
+ onEdit,
+ onDelete,
+ proxyBaseUrl,
+}) => {
+ const [sorting, setSorting] = useState([]);
+ const [expanded, setExpanded] = useState({});
const baseUrl = resolveBaseUrl(proxyBaseUrl);
- const columns: ColumnsType = [
- {
- title: "GROUP NAME",
- dataIndex: "group_name",
- key: "group_name",
- render: (name: string) => (
-
- {name}
-
- ),
- },
- {
- title: "MODELS",
- dataIndex: "models",
- key: "models",
- render: (models: string[]) => (
-
- {models.map((m) => (
- {m}
- ))}
-
- ),
- },
- {
- title: "STRATEGY",
- dataIndex: "routing_strategy",
- key: "routing_strategy",
- render: (strategy: string) => (
-
-
- {formatStrategyLabel(strategy)}
-
- ),
- },
- {
- title: "ACTIONS",
- key: "actions",
- width: 120,
- align: "right",
- render: (_, group) => (
-
-
- }
- onClick={(e) => {
- e.stopPropagation();
- onEdit(group);
- }}
- />
-
-
- }
- onClick={(e) => {
- e.stopPropagation();
- onDelete(group);
- }}
- />
-
-
- ),
- },
- ];
+ const toggleUsage = useCallback((group: RoutingGroup) => {
+ setExpanded((previous) => {
+ const current = previous === true ? {} : previous;
+ return { ...current, [group.group_name]: current[group.group_name] !== true };
+ });
+ }, []);
+
+ const columns = useMemo(() => {
+ const deps = { onEdit, onDelete, onToggleUsage: toggleUsage };
+ return getRoutingGroupsTableColumns(deps);
+ }, [onEdit, onDelete, toggleUsage]);
return (
-
- rowKey="group_name"
+ setExpandedRowKeys([...keys]),
- expandedRowRender: (group) => (
-
-
-
- How routing works for this group
-
-
- Callers request any model in the group by name — LiteLLM picks a deployment behind the scenes using the{" "}
- {formatStrategyLabel(group.routing_strategy)} strategy.
-
-
-
- ),
- }}
+ getRowId={(group) => group.group_name}
+ sortingMode="client"
+ sorting={sorting}
+ onSortingChange={setSorting}
+ expanded={expanded}
+ onExpandedChange={setExpanded}
+ getRowCanExpand={() => true}
+ renderSubComponent={({ row }) => }
+ isLoading={isLoading}
+ loadingMessage="Loading routing groups…"
+ noDataMessage={ }
+ size="compact"
/>
);
};
diff --git a/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTableColumns.tsx b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTableColumns.tsx
new file mode 100644
index 00000000000..586df60c4f7
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/routing_groups/RoutingGroupsTableColumns.tsx
@@ -0,0 +1,111 @@
+"use client";
+
+import type { ColumnDef } from "@tanstack/react-table";
+import { GitBranch, MoreHorizontal, Pencil, Trash2 } from "lucide-react";
+
+import { DataTableSortHeader } from "@/components/shared/DataTable";
+import { IdentityCell, ModelsCell } from "@/components/shared/table_cells";
+import { buttonVariants } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { cn } from "@/lib/cva.config";
+
+import { formatStrategyLabel } from "./strategy";
+import type { RoutingGroup } from "./types";
+
+interface RoutingGroupRowActionsProps {
+ group: RoutingGroup;
+ onEdit: (group: RoutingGroup) => void;
+ onDelete: (group: RoutingGroup) => void;
+}
+
+function RoutingGroupRowActions({ group, onEdit, onDelete }: RoutingGroupRowActionsProps) {
+ return (
+
+
+
+
+
+ onEdit(group)}>
+
+ Edit
+
+ onDelete(group)}
+ >
+
+ Delete
+
+
+
+ );
+}
+
+interface RoutingGroupsTableColumnsDeps {
+ onEdit: (group: RoutingGroup) => void;
+ onDelete: (group: RoutingGroup) => void;
+ onToggleUsage: (group: RoutingGroup) => void;
+}
+
+export const getRoutingGroupsTableColumns = ({
+ onEdit,
+ onDelete,
+ onToggleUsage,
+}: RoutingGroupsTableColumnsDeps): ColumnDef[] => [
+ {
+ id: "group_name",
+ accessorKey: "group_name",
+ meta: { title: "Group Name", skeleton: "text" },
+ header: ({ column }) => ,
+ size: 240,
+ enableSorting: true,
+ cell: ({ row }) => (
+ onToggleUsage(row.original)} />
+ ),
+ },
+ {
+ id: "models",
+ meta: { title: "Models", skeleton: "chips" },
+ header: "Models",
+ size: 320,
+ enableSorting: false,
+ cell: ({ row }) => ,
+ },
+ {
+ id: "routing_strategy",
+ accessorKey: "routing_strategy",
+ meta: { title: "Strategy", skeleton: "text" },
+ header: ({ column }) => ,
+ size: 180,
+ enableSorting: true,
+ cell: ({ row }) => (
+
+
+ {formatStrategyLabel(row.original.routing_strategy)}
+
+ ),
+ },
+ {
+ id: "actions",
+ meta: { className: "text-right", headerClassName: "text-right" },
+ header: () => Actions ,
+ size: 64,
+ enableSorting: false,
+ enableHiding: false,
+ cell: ({ row }) => (
+
+
+
+ ),
+ },
+];
diff --git a/ui/litellm-dashboard/src/components/routing_groups/index.tsx b/ui/litellm-dashboard/src/components/routing_groups/index.tsx
index 1ee281bd92a..8b33b050377 100644
--- a/ui/litellm-dashboard/src/components/routing_groups/index.tsx
+++ b/ui/litellm-dashboard/src/components/routing_groups/index.tsx
@@ -126,7 +126,7 @@ const RoutingGroups: React.FC = () => {
setDeletingGroup(g)}
proxyBaseUrl={proxySettings.LITELLM_UI_API_DOC_BASE_URL?.trim() || proxySettings.PROXY_BASE_URL || ""}
diff --git a/ui/litellm-dashboard/src/components/routing_groups/strategy.ts b/ui/litellm-dashboard/src/components/routing_groups/strategy.ts
new file mode 100644
index 00000000000..8fa3a8f4dcd
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/routing_groups/strategy.ts
@@ -0,0 +1,8 @@
+const STRATEGY_LABELS: Readonly> = {
+ "simple-shuffle": "Simple Shuffle",
+ "least-busy": "Least Busy",
+ "usage-based-routing": "Usage Based",
+ "latency-based-routing": "Latency Based",
+};
+
+export const formatStrategyLabel = (strategy: string): string => STRATEGY_LABELS[strategy] ?? strategy;