mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
fix(ui): keep the team model select loading until /team/info reports the org ceiling
useTeam seeds its cache from the team list, which has no organization_models, so the select briefly rendered unfiltered. Also drop the Prisma include assertion from the backend test. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
3f42a4fc98
commit
5ddb0fe691
3 changed files with 52 additions and 6 deletions
|
|
@ -14224,10 +14224,6 @@ async def test_team_info_returns_parent_organization_models(organization, expect
|
|||
user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN),
|
||||
)
|
||||
|
||||
include = mock_prisma.db.litellm_teamtable.find_unique.await_args.kwargs["include"]
|
||||
assert include["litellm_organization_table"] is True
|
||||
|
||||
team_info = response["team_info"]
|
||||
assert team_info.organization_models == expected_models
|
||||
# the org row itself carries budgets and spend; only its model list may ride along
|
||||
assert "litellm_organization_table" not in team_info.model_dump()
|
||||
|
|
|
|||
|
|
@ -473,6 +473,50 @@ describe("ModelSelect", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("should stay in the loading state while a list-seeded team is still fetching its org ceiling", () => {
|
||||
mockUseOrganization.mockReturnValue({ data: undefined, isLoading: false } as any);
|
||||
mockUseTeam.mockReturnValue({
|
||||
data: { team_id: "team-1", models: [] },
|
||||
isLoading: false,
|
||||
isFetching: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
context="team"
|
||||
teamID="team-1"
|
||||
organizationID="org-1"
|
||||
options={{ includeSpecialOptions: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryAllByRole("combobox")).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should not hold the loading state on a background refetch once the org ceiling is known", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseOrganization.mockReturnValue({ data: undefined, isLoading: false } as any);
|
||||
mockUseTeam.mockReturnValue({
|
||||
data: { team_id: "team-1", organization_models: ["all-proxy-models"] },
|
||||
isLoading: false,
|
||||
isFetching: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
context="team"
|
||||
teamID="team-1"
|
||||
organizationID="org-1"
|
||||
options={{ includeSpecialOptions: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openModelList(user);
|
||||
expectOffered("All Proxy Models");
|
||||
});
|
||||
|
||||
it("should keep hiding All Proxy Models when neither the team nor the org reports a ceiling", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockUseOrganization.mockReturnValue({ data: undefined, isLoading: false } as any);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from "@/components/ui/combobox";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import type { Team } from "@/components/key_team_helpers/key_list";
|
||||
import { splitWildcardModels } from "./modelUtils";
|
||||
|
||||
const MODEL_SELECT_ALL_PROXY_MODELS_SPECIAL_VALUE = {
|
||||
|
|
@ -76,6 +77,10 @@ type FilterContextArgs = {
|
|||
const isUncappedModelCeiling = (organizationModels: string[]) =>
|
||||
organizationModels.length === 0 || organizationModels.includes(MODEL_SELECT_ALL_PROXY_MODELS_SPECIAL_VALUE.value);
|
||||
|
||||
// useTeam seeds from the team list, which omits organization_models; /team/info is the only source of the org ceiling.
|
||||
const isAwaitingOrganizationModels = (team: Team | undefined, isFetchingTeam: boolean) =>
|
||||
isFetchingTeam && team !== undefined && team.organization_models === undefined;
|
||||
|
||||
const contextFilters: Record<ModelSelectProps["context"], (args: FilterContextArgs) => string[]> = {
|
||||
user: ({ allProxyModels, userModels, options }) => {
|
||||
if (!userModels) return [];
|
||||
|
|
@ -118,13 +123,14 @@ export const ModelSelect = (props: ModelSelectProps) => {
|
|||
const { id, teamID, organizationID, options, context, dataTestId, value = [], onChange, style } = props;
|
||||
const { showAllProxyModelsOverride, includeSpecialOptions } = options || {};
|
||||
const { data: allProxyModels, isLoading: isLoadingAllProxyModels } = useAllProxyModels();
|
||||
const { data: team, isLoading: isLoadingTeam } = useTeam(teamID);
|
||||
const { data: team, isLoading: isLoadingTeam, isFetching: isFetchingTeam } = useTeam(teamID);
|
||||
const { data: organization, isLoading: isLoadingOrganization } = useOrganization(organizationID);
|
||||
const { data: currentUser, isLoading: isCurrentUserLoading } = useCurrentUser();
|
||||
|
||||
const isSpecialOption = (value: string) => MODEL_SENTINEL_OPTIONS.some((sv) => sv.value === value);
|
||||
const hasSpecialOptionSelected = value.some(isSpecialOption);
|
||||
const isLoading = isLoadingAllProxyModels || isLoadingTeam || isLoadingOrganization || isCurrentUserLoading;
|
||||
const isTeamPending = isLoadingTeam || isAwaitingOrganizationModels(team, isFetchingTeam);
|
||||
const isLoading = isLoadingAllProxyModels || isTeamPending || isLoadingOrganization || isCurrentUserLoading;
|
||||
// The org's ceiling rides on /team/info, which a team admin may read; /organization/info 403s for them.
|
||||
const organizationModels = team?.organization_models ?? organization?.models;
|
||||
const organizationHasAllProxyModels = organizationModels !== undefined && isUncappedModelCeiling(organizationModels);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue