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:
shivam 2026-07-20 23:11:58 +00:00
parent 9ac6cc7bee
commit ca9648171d
3 changed files with 56 additions and 6 deletions

View file

@ -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} />);

View file

@ -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"] => {

View file

@ -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>