diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.test.tsx index 0aef19ce110..e342ee33f25 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.test.tsx @@ -46,6 +46,7 @@ vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ { model_name: "gpt-auto", litellm_params: { model: "auto_router/gpt-auto" } }, ], })), + usePlainModelGroups: vi.fn(() => new Set(["prod-claude"])), })); vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ @@ -388,14 +389,15 @@ describe("ShadowEvalSection", () => { expect(screen.getByText("Start shadow eval")).toBeDisabled(); await user.click(screen.getByPlaceholderText("Select a baseline model")); - await user.click(await screen.findByRole("option", { name: /openai\/gpt-4o/ })); + expect(await screen.findByRole("option", { name: /openai\/gpt-4o/ })).toBeInTheDocument(); + await user.click(screen.getByRole("option", { name: /prod-claude/ })); await user.click(screen.getByText("Start shadow eval")); const expectedBody = { api_key_id: "hash-alpha", router_name: "gpt-auto", direction: "reverse", - baseline_model: "openai/gpt-4o", + baseline_model: "prod-claude", shadow_percentage: 10, duration_days: 7, max_turns: 200, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.tsx index 17186eaca50..df2989d3990 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/ShadowEvalSection.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useState } from "react"; import { useInfiniteKeys } from "@/app/(dashboard)/hooks/keys/useKeys"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { useModelCostMap } from "@/app/(dashboard)/hooks/models/useModelCostMap"; -import { useAutoRouters } from "@/app/(dashboard)/hooks/models/useModels"; +import { useAutoRouters, usePlainModelGroups } from "@/app/(dashboard)/hooks/models/useModels"; import { PaginatedSearchSelect } from "@/components/shared/PaginatedSearchSelect"; import { SearchSelect, type SearchSelectOption } from "@/components/shared/SearchSelect"; import { Badge } from "@/components/ui/badge"; @@ -274,8 +274,17 @@ const useJudgeModelOptions = (): SearchSelectOption[] => { }; const useBaselineModelOptions = (): SearchSelectOption[] => { + const configuredGroups = usePlainModelGroups(); const chatModels = useChatModelNames(); - return useMemo(() => chatModels.map((model) => ({ label: model, value: model })), [chatModels]); + return useMemo(() => { + const configured = [...configuredGroups] + .toSorted((a, b) => a.localeCompare(b)) + .map((model) => ({ label: model, value: model, sublabel: "Configured on this gateway" })); + const rest = chatModels + .filter((model) => !configuredGroups.has(model)) + .map((model) => ({ label: model, value: model })); + return [...configured, ...rest]; + }, [configuredGroups, chatModels]); }; const DIRECTION_OPTIONS: readonly { value: ShadowEvalDirection; label: string }[] = [ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts index f04c4b7bfcd..411e8402e11 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { isAutoRouterDeployment, selectAutoRouterModelGroups, + selectPlainModelGroups, useAllProxyModels, useAutoRouterModelGroups, useAutoRouters, @@ -977,6 +978,32 @@ describe("selectAutoRouterModelGroups", () => { }); }); +describe("selectPlainModelGroups", () => { + it("keeps only non-auto-router model groups", () => { + const deployments: AutoRouterCandidateDeployment[] = [ + { model_name: "smart-router", litellm_params: { model: "auto_router/complexity_router" } }, + { model_name: "claude-haiku", litellm_params: { model: "anthropic/claude-haiku-4-5" } }, + { model_name: "claude-sonnet", litellm_params: { model: "anthropic/claude-sonnet-4-5" } }, + { model_name: "cheap-router", litellm_params: { model: "auto_router/adaptive_router" } }, + ]; + + expect(selectPlainModelGroups(deployments)).toEqual(new Set(["claude-haiku", "claude-sonnet"])); + }); + + it("drops a group name that also fronts an auto-router deployment", () => { + const deployments: AutoRouterCandidateDeployment[] = [ + { model_name: "shared-name", litellm_params: { model: "auto_router/complexity_router" } }, + { model_name: "shared-name", litellm_params: { model: "anthropic/claude-sonnet-4-5" } }, + ]; + + expect(selectPlainModelGroups(deployments)).toEqual(new Set()); + }); + + it("drops deployments that have no public model_name", () => { + expect(selectPlainModelGroups([{ model_name: "", litellm_params: { model: "openai/gpt-4o" } }])).toEqual(new Set()); + }); +}); + describe("useAutoRouterModelGroups", () => { let queryClient: QueryClient; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts index 68044a45edc..a5fbc433ea3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.ts @@ -123,6 +123,16 @@ export const selectAutoRouterModelGroups = (deployments: AutoRouterCandidateDepl export const selectAutoRouterDeployments = (deployments: AutoRouterDeployment[]): AutoRouterDeployment[] => deployments.filter(isAutoRouterDeployment); +export const selectPlainModelGroups = (deployments: AutoRouterCandidateDeployment[]): ReadonlySet => { + const autoRouterGroups = selectAutoRouterModelGroups(deployments); + return new Set( + deployments + .map((deployment) => deployment.model_name) + .filter((modelName): modelName is string => Boolean(modelName)) + .filter((modelName) => !autoRouterGroups.has(modelName)), + ); +}; + export const fetchAllModelDeployments = async ( accessToken: string, userId: string, @@ -172,6 +182,17 @@ export const useAutoRouterModelGroups = (): ReadonlySet => { return data ?? NO_AUTO_ROUTERS; }; +export const usePlainModelGroups = (): ReadonlySet => { + const { accessToken, userId, userRole } = useAuthorized(); + const { data } = useQuery>({ + queryKey: autoRouterListKey(userId, userRole), + queryFn: async () => await fetchAllModelDeployments(accessToken!, userId!, userRole!), + enabled: Boolean(accessToken && userId && userRole), + select: selectPlainModelGroups, + }); + return data ?? NO_AUTO_ROUTERS; +}; + export const useAutoRouters = (): UseQueryResult => { const { accessToken, userId, userRole } = useAuthorized(); return useQuery({