mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
fix(ui): scope team router-settings fallback dropdown to team models
The team Router Settings -> Fallbacks picker sourced its options from the global /model_group/info list, which excludes team-scoped BYOK models, so a team whose models are all team-scoped showed no selectable models. Pass the teamId into RouterSettingsAccordion and load the team's public model names from /v2/model/info?include_team_models=true&teamId=<team> when scoped. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
9ac6cc7bee
commit
ca9648171d
3 changed files with 56 additions and 6 deletions
|
|
@ -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[] }) => (
|
||||
<div data-testid="available-models">{availableModels.join(",")}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
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<typeof vi.fn>) => {
|
||||
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(<RouterSettingsAccordion accessToken="test-token" teamId="team-1" />);
|
||||
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(<RouterSettingsAccordion accessToken="test-token" />);
|
||||
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(<RouterSettingsAccordion accessToken="test-token" onChange={onChange} />);
|
||||
|
|
|
|||
|
|
@ -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<RouterSettingsAccordionRef, RouterSettingsAccordionProps>(
|
||||
({ accessToken, value, onChange, modelData }, ref) => {
|
||||
({ accessToken, value, onChange, modelData, teamId }, ref) => {
|
||||
const [formValue, setFormValue] = useState<RouterSettingsFormValue>({
|
||||
routerSettings: {},
|
||||
selectedStrategy: null,
|
||||
|
|
@ -182,6 +183,15 @@ const RouterSettingsAccordion = forwardRef<RouterSettingsAccordionRef, RouterSet
|
|||
}
|
||||
const loadModels = async () => {
|
||||
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<RouterSettingsAccordionRef, RouterSet
|
|||
}
|
||||
};
|
||||
loadModels();
|
||||
}, [accessToken]);
|
||||
}, [accessToken, teamId]);
|
||||
|
||||
// Helper function to build router_settings from current state
|
||||
const buildRouterSettings = (): RouterSettingsAccordionValue["router_settings"] => {
|
||||
|
|
|
|||
|
|
@ -1246,6 +1246,7 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<RouterSettingsAccordion
|
||||
ref={routerSettingsRef}
|
||||
accessToken={accessToken || ""}
|
||||
teamId={info.team_id}
|
||||
value={info.router_settings ? { router_settings: info.router_settings } : undefined}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue