diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx
index 6891082b47e..1f933d42e06 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.test.tsx
@@ -113,12 +113,15 @@ const MOCK_AUTHORIZED = {
};
const mockSetSelectedModelGroup = vi.fn();
+const mockSetSelectedModelAccessGroupFilter = vi.fn();
const mockSetSelectedModelId = vi.fn();
const mockSetSelectedTeamId = vi.fn();
const defaultProps = {
selectedModelGroup: "all",
setSelectedModelGroup: mockSetSelectedModelGroup,
+ selectedModelAccessGroupFilter: null,
+ setSelectedModelAccessGroupFilter: mockSetSelectedModelAccessGroupFilter,
availableModelGroups: ["gpt-4", "gpt-3.5-turbo"],
availableModelAccessGroups: ["sales-team"],
setSelectedModelId: mockSetSelectedModelId,
@@ -263,6 +266,35 @@ describe("AllModelsTab", () => {
expect(within(table).queryByText("gpt-4")).not.toBeInTheDocument();
});
+ it("filters the fetched page down to the selected access group", () => {
+ setModelsInfo(
+ [
+ makeRow(),
+ {
+ ...makeRow({ model_info: { id: "model-2", access_groups: ["sales-team"] } }),
+ model_name: "claude-opus",
+ },
+ ],
+ 2,
+ );
+ render();
+
+ const table = screen.getByRole("table");
+ expect(within(table).getByText("claude-opus")).toBeInTheDocument();
+ expect(within(table).queryByText("gpt-4")).not.toBeInTheDocument();
+ });
+
+ it("removing the access group filter chip clears it through the setter", async () => {
+ const user = userEvent.setup();
+ render();
+
+ await user.click(screen.getByTestId("filter-chip-remove-model_info_access_groups"));
+
+ await waitFor(() => {
+ expect(mockSetSelectedModelAccessGroupFilter).toHaveBeenCalledWith(null);
+ });
+ });
+
it("keeps an unknown model group (for example a stale deep link) visible as a removable filter chip", async () => {
const user = userEvent.setup();
setModelsInfo([makeRow()], 1);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
index 1dc7736d5ac..b56e3fcb19d 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AllModelsTab.tsx
@@ -32,6 +32,8 @@ const DEFAULT_PAGINATION: PaginationState = { pageIndex: 0, pageSize: DEFAULT_PA
interface AllModelsTabProps {
selectedModelGroup: string | null;
setSelectedModelGroup: (selectedModelGroup: string) => void;
+ selectedModelAccessGroupFilter: string | null;
+ setSelectedModelAccessGroupFilter: (selectedModelAccessGroupFilter: string | null) => void;
availableModelGroups: string[];
availableModelAccessGroups: string[];
setSelectedModelId: (id: string) => void;
@@ -41,6 +43,8 @@ interface AllModelsTabProps {
const AllModelsTab = ({
selectedModelGroup,
setSelectedModelGroup,
+ selectedModelAccessGroupFilter,
+ setSelectedModelAccessGroupFilter,
availableModelGroups,
availableModelAccessGroups,
setSelectedModelId,
@@ -55,7 +59,6 @@ const AllModelsTab = ({
const [debouncedSearch, setDebouncedSearch] = useState("");
const [modelViewMode, setModelViewMode] = useState("current_team");
const [selectedTeamValue, setSelectedTeamValue] = useState(PERSONAL_TEAM_VALUE);
- const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useState(null);
const [pagination, setPagination] = useState(DEFAULT_PAGINATION);
const [sorting, setSorting] = useState([]);
const [isModelSettingsModalVisible, setIsModelSettingsModalVisible] = useState(false);
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx
index 3b2c0a84ea2..454b2abfecb 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.test.tsx
@@ -7,6 +7,8 @@ import AllModelsPanel from "./AllModelsPanel";
interface CapturedTabProps {
selectedModelGroup: string | null;
setSelectedModelGroup: (modelGroup: string) => void;
+ selectedModelAccessGroupFilter: string | null;
+ setSelectedModelAccessGroupFilter: (accessGroup: string | null) => void;
}
const captured: { props: CapturedTabProps | null } = { props: null };
@@ -59,3 +61,47 @@ describe("AllModelsPanel ?model_group= filter", () => {
spy.mockRestore();
});
});
+
+describe("AllModelsPanel ?model_access_group= filter", () => {
+ beforeEach(() => {
+ captured.props = null;
+ window.history.pushState(null, "", "/models-and-endpoints/");
+ });
+
+ it("seeds the access group filter from the URL", () => {
+ window.history.pushState(null, "", "/models-and-endpoints/?model_access_group=sales-team");
+ render();
+ expect(captured.props?.selectedModelAccessGroupFilter).toBe("sales-team");
+ });
+
+ it("writes the selected access group to the URL via replaceState", () => {
+ render();
+ const spy = vi.spyOn(window.history, "replaceState");
+ act(() => captured.props?.setSelectedModelAccessGroupFilter("sales-team"));
+ expect(spy.mock.calls.at(-1)?.[2]).toContain("model_access_group=sales-team");
+ spy.mockRestore();
+ });
+
+ it("removes the param when the access group filter clears", () => {
+ window.history.pushState(null, "", "/models-and-endpoints/?model_access_group=sales-team");
+ render();
+ const spy = vi.spyOn(window.history, "replaceState");
+ act(() => captured.props?.setSelectedModelAccessGroupFilter(null));
+ expect(spy.mock.calls.at(-1)?.[2] as string).not.toContain("model_access_group");
+ spy.mockRestore();
+ });
+
+ it("the two filter params stay independent", () => {
+ window.history.pushState(null, "", "/models-and-endpoints/?model_group=gpt-4o&model_access_group=sales-team");
+ render();
+ expect(captured.props?.selectedModelGroup).toBe("gpt-4o");
+ expect(captured.props?.selectedModelAccessGroupFilter).toBe("sales-team");
+
+ const spy = vi.spyOn(window.history, "replaceState");
+ act(() => captured.props?.setSelectedModelAccessGroupFilter(null));
+ const url = spy.mock.calls.at(-1)?.[2] as string;
+ expect(url).toContain("model_group=gpt-4o");
+ expect(url).not.toContain("model_access_group");
+ spy.mockRestore();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx
index f6021e92ede..fd6a4607124 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx
@@ -8,18 +8,27 @@ import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/us
import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation";
import { navigateWithParams } from "@/app/(dashboard)/navigateWithParams";
-export default function AllModelsPanel() {
+const useUrlFilter = (paramName: string): [string | null, (value: string | null) => void] => {
const searchParams = useSearchParams();
- const selectedModelGroup = searchParams?.get("model_group") ?? null;
- const setSelectedModelGroup = useCallback((modelGroup: string) => {
- navigateWithParams((params) => {
- if (modelGroup && modelGroup !== ALL_MODEL_GROUPS_VALUE) {
- params.set("model_group", modelGroup);
- } else {
- params.delete("model_group");
- }
- }, "replace");
- }, []);
+ const value = searchParams?.get(paramName) ?? null;
+ const setValue = useCallback(
+ (nextValue: string | null) => {
+ navigateWithParams((params) => {
+ if (nextValue && nextValue !== ALL_MODEL_GROUPS_VALUE) {
+ params.set(paramName, nextValue);
+ } else {
+ params.delete(paramName);
+ }
+ }, "replace");
+ },
+ [paramName],
+ );
+ return [value, setValue];
+};
+
+export default function AllModelsPanel() {
+ const [selectedModelGroup, setSelectedModelGroup] = useUrlFilter("model_group");
+ const [selectedModelAccessGroupFilter, setSelectedModelAccessGroupFilter] = useUrlFilter("model_access_group");
const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData();
const { openModel, openTeam } = useModelDetailRouting();
@@ -27,6 +36,8 @@ export default function AllModelsPanel() {