diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx index a70b7602e5b..5d55a8b014f 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.test.tsx @@ -1,11 +1,14 @@ -import { act, fireEvent, render, screen } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; import type { ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { RouterSettingsFormValue } from "../router_settings/RouterSettingsForm"; +import { modelInfoCall } from "../networking"; +import { fetchAvailableModels } from "@/components/llm_calls/fetch_models"; import RouterSettingsAccordion, { RouterSettingsAccordionValue } from "./RouterSettingsAccordion"; vi.mock("../networking", () => ({ getRouterSettingsCall: vi.fn().mockResolvedValue({}), + modelInfoCall: vi.fn().mockResolvedValue({ data: [] }), })); vi.mock("@/components/llm_calls/fetch_models", () => ({ @@ -13,7 +16,9 @@ vi.mock("@/components/llm_calls/fetch_models", () => ({ })); vi.mock("../Settings/RouterSettings/Fallbacks/FallbackSelectionForm", () => ({ - FallbackSelectionForm: () => null, + FallbackSelectionForm: ({ availableModels }: { availableModels: string[] }) => ( +
{availableModels.join(",")}
+ ), })); vi.mock("@tremor/react", () => ({ @@ -41,14 +46,25 @@ vi.mock("../router_settings/RouterSettingsForm", () => ({ describe("RouterSettingsAccordion", () => { beforeEach(() => { + vi.clearAllMocks(); vi.useFakeTimers(); }); afterEach(() => { - vi.runOnlyPendingTimers(); + cleanup(); + if (vi.isFakeTimers()) { + vi.runOnlyPendingTimers(); + } vi.useRealTimers(); }); + const flushPromises = async () => { + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + }; + const flushInitialPropagation = async (onChange: ReturnType) => { await act(async () => { vi.advanceTimersByTime(100); @@ -81,6 +97,29 @@ describe("RouterSettingsAccordion", () => { expect(onChange.mock.calls[0][0].router_settings.routing_strategy).toBe("usage-based-routing"); }); + it("populates the fallback dropdown with team-scoped models when teamId is set", async () => { + vi.mocked(modelInfoCall).mockResolvedValueOnce({ + data: [{ model_name: "team-a-model" }, { model_name: "team-b-model" }], + }); + + render(); + await flushPromises(); + + expect(screen.getByTestId("available-models").textContent).toBe("team-a-model,team-b-model"); + expect(modelInfoCall).toHaveBeenCalledWith("test-token", "", "", 1, 1000, undefined, undefined, "team-1"); + expect(fetchAvailableModels).not.toHaveBeenCalled(); + }); + + it("uses the global model list when no teamId is provided", async () => { + vi.mocked(fetchAvailableModels).mockResolvedValueOnce([{ model_group: "shared-gpt" }]); + + render(); + await flushPromises(); + + expect(screen.getByTestId("available-models").textContent).toBe("shared-gpt"); + expect(modelInfoCall).not.toHaveBeenCalled(); + }); + it("does not call onChange when unmounted mid-wait", async () => { const onChange = vi.fn<(value: RouterSettingsAccordionValue) => void>(); const { unmount } = render(); diff --git a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx index 08b917e302f..81e07592705 100644 --- a/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx +++ b/ui/litellm-dashboard/src/components/common_components/RouterSettingsAccordion.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState, useImperativeHandle, forwardRef, useRef } from "react"; import { TabPanel, TabPanels, TabGroup, TabList, Tab } from "@tremor/react"; import { useDebouncedCallback } from "@tanstack/react-pacer/debouncer"; -import { getRouterSettingsCall } from "../networking"; +import { getRouterSettingsCall, modelInfoCall } from "../networking"; import RouterSettingsForm, { RouterSettingsFormValue } from "../router_settings/RouterSettingsForm"; import { Fallbacks } from "../Settings/RouterSettings/Fallbacks/AddFallbacks"; import { FallbackSelectionForm } from "../Settings/RouterSettings/Fallbacks/FallbackSelectionForm"; @@ -30,6 +30,7 @@ interface RouterSettingsAccordionProps { value?: RouterSettingsAccordionValue; onChange?: (value: RouterSettingsAccordionValue) => void; modelData?: any; + teamId?: string; } export interface RouterSettingsAccordionRef { @@ -39,7 +40,7 @@ export interface RouterSettingsAccordionRef { const PROPAGATE_WAIT_MS = 100; const RouterSettingsAccordion = forwardRef( - ({ accessToken, value, onChange, modelData }, ref) => { + ({ accessToken, value, onChange, modelData, teamId }, ref) => { const [formValue, setFormValue] = useState({ routerSettings: {}, selectedStrategy: null, @@ -182,6 +183,15 @@ const RouterSettingsAccordion = forwardRef { try { + if (teamId) { + const response = await modelInfoCall(accessToken, "", "", 1, 1000, undefined, undefined, teamId); + const teamModels: ModelGroup[] = (response?.data ?? []) + .map((item: { model_name?: string }) => item.model_name) + .filter((name: string | undefined): name is string => Boolean(name)) + .map((name: string) => ({ model_group: name })); + setModelInfo(teamModels); + return; + } const uniqueModels = await fetchAvailableModels(accessToken); setModelInfo(uniqueModels); } catch (error) { @@ -189,7 +199,7 @@ const RouterSettingsAccordion = forwardRef { diff --git a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx index eaf2faa08ee..69b10a57738 100644 --- a/ui/litellm-dashboard/src/components/team/TeamInfo.tsx +++ b/ui/litellm-dashboard/src/components/team/TeamInfo.tsx @@ -1246,6 +1246,7 @@ const TeamInfoView: React.FC = ({