mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Make model select required for team, add checks for all-proxy-models
This commit is contained in:
parent
5cb5c2a7b7
commit
5ec3f19a53
4 changed files with 225 additions and 70 deletions
|
|
@ -1,5 +1,6 @@
|
|||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fetchAvailableModelsForTeamOrKey } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import { teamCreateCall } from "./networking";
|
||||
import OldTeams from "./OldTeams";
|
||||
|
||||
|
|
@ -23,6 +24,28 @@ vi.mock("./molecules/notifications_manager", () => ({
|
|||
},
|
||||
}));
|
||||
|
||||
vi.mock("./key_team_helpers/fetch_available_models_team_key", () => ({
|
||||
fetchAvailableModelsForTeamOrKey: vi.fn(),
|
||||
getModelDisplayName: vi.fn((model: string) => model),
|
||||
unfurlWildcardModelsInList: vi.fn((teamModels: string[], allModels: string[]) => {
|
||||
const wildcardDisplayNames: string[] = [];
|
||||
const expandedModels: string[] = [];
|
||||
|
||||
teamModels.forEach((teamModel) => {
|
||||
if (teamModel.endsWith("/*")) {
|
||||
const provider = teamModel.replace("/*", "");
|
||||
const matchingModels = allModels.filter((model) => model.startsWith(provider + "/"));
|
||||
expandedModels.push(...matchingModels);
|
||||
wildcardDisplayNames.push(teamModel);
|
||||
} else {
|
||||
expandedModels.push(teamModel);
|
||||
}
|
||||
});
|
||||
|
||||
return [...wildcardDisplayNames, ...expandedModels].filter((item, index, array) => array.indexOf(item) === index);
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("OldTeams - handleCreate organization handling", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
|
@ -236,7 +259,7 @@ describe("OldTeams - handleCreate organization handling", () => {
|
|||
});
|
||||
|
||||
it("should clear the delete modal when the cancel button is clicked", async () => {
|
||||
const { getByRole, getByTestId } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[
|
||||
{
|
||||
|
|
@ -261,7 +284,7 @@ describe("OldTeams - handleCreate organization handling", () => {
|
|||
organizations={[]}
|
||||
/>,
|
||||
);
|
||||
const deleteTeamButton = getByTestId("delete-team-button");
|
||||
const deleteTeamButton = screen.getByTestId("delete-team-button");
|
||||
act(() => {
|
||||
fireEvent.click(deleteTeamButton);
|
||||
});
|
||||
|
|
@ -275,7 +298,7 @@ describe("OldTeams - empty state", () => {
|
|||
});
|
||||
|
||||
it("should display empty state message when teams array is empty", () => {
|
||||
const { getByText } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[]}
|
||||
searchParams={{}}
|
||||
|
|
@ -287,12 +310,12 @@ describe("OldTeams - empty state", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(getByText("No teams found")).toBeInTheDocument();
|
||||
expect(getByText("Adjust your filters or create a new team")).toBeInTheDocument();
|
||||
expect(screen.getByText("No teams found")).toBeInTheDocument();
|
||||
expect(screen.getByText("Adjust your filters or create a new team")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display empty state message when teams is null", () => {
|
||||
const { getByText } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={null}
|
||||
searchParams={{}}
|
||||
|
|
@ -304,12 +327,12 @@ describe("OldTeams - empty state", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(getByText("No teams found")).toBeInTheDocument();
|
||||
expect(getByText("Adjust your filters or create a new team")).toBeInTheDocument();
|
||||
expect(screen.getByText("No teams found")).toBeInTheDocument();
|
||||
expect(screen.getByText("Adjust your filters or create a new team")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not display empty state when teams array has items", () => {
|
||||
const { queryByText, getByText } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[
|
||||
{
|
||||
|
|
@ -335,9 +358,9 @@ describe("OldTeams - empty state", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText("No teams found")).not.toBeInTheDocument();
|
||||
expect(queryByText("Adjust your filters or create a new team")).not.toBeInTheDocument();
|
||||
expect(getByText("Test Team")).toBeInTheDocument();
|
||||
expect(screen.queryByText("No teams found")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Adjust your filters or create a new team")).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Test Team")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -473,7 +496,7 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
|
|||
});
|
||||
|
||||
it("should show Default Team Settings tab for Admin role", () => {
|
||||
const { getByRole } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[
|
||||
{
|
||||
|
|
@ -499,11 +522,11 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Default Team Settings tab for proxy_admin role", () => {
|
||||
const { getByRole } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[
|
||||
{
|
||||
|
|
@ -529,11 +552,11 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("tab", { name: "Default Team Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show Default Team Settings tab for proxy_admin_viewer role", () => {
|
||||
const { queryByRole } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[
|
||||
{
|
||||
|
|
@ -559,11 +582,11 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show Default Team Settings tab for Admin Viewer role", () => {
|
||||
const { queryByRole } = render(
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[
|
||||
{
|
||||
|
|
@ -589,6 +612,44 @@ describe("OldTeams - Default Team Settings tab visibility", () => {
|
|||
/>,
|
||||
);
|
||||
|
||||
expect(queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("tab", { name: "Default Team Settings" })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("OldTeams - all-proxy-models dropdown visibility", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
|
||||
});
|
||||
|
||||
it("should not show all-proxy-models option when user has no access to it", async () => {
|
||||
vi.mocked(fetchAvailableModelsForTeamOrKey).mockResolvedValue(["gpt-4", "gpt-3.5-turbo"]);
|
||||
|
||||
render(
|
||||
<OldTeams
|
||||
teams={[]}
|
||||
searchParams={{}}
|
||||
accessToken="test-token"
|
||||
setTeams={vi.fn()}
|
||||
userID="user-123"
|
||||
userRole="Admin"
|
||||
organizations={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchAvailableModelsForTeamOrKey).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const createButton = screen.getByRole("button", { name: /create new team/i });
|
||||
act(() => {
|
||||
fireEvent.click(createButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/models/i)).toBeInTheDocument();
|
||||
});
|
||||
const allProxyModelsOption = screen.queryByText("All Proxy Models");
|
||||
expect(allProxyModelsOption).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1139,12 +1139,20 @@ const Teams: React.FC<TeamProps> = ({
|
|||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please select at least one model",
|
||||
},
|
||||
]}
|
||||
name="models"
|
||||
>
|
||||
<Select2 mode="multiple" placeholder="Select models" style={{ width: "100%" }}>
|
||||
<Select2.Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
</Select2.Option>
|
||||
{(isProxyAdminRole(userRole || "") || userModels.includes("all-proxy-models")) && (
|
||||
<Select2.Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
</Select2.Option>
|
||||
)}
|
||||
<Select2.Option key="no-default-models" value="no-default-models">
|
||||
No Default Models
|
||||
</Select2.Option>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import * as networking from "@/components/networking";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import TeamInfoView from "./team_info";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
import * as networking from "@/components/networking";
|
||||
|
||||
// Mock the networking module
|
||||
vi.mock("@/components/networking", () => ({
|
||||
|
|
@ -61,7 +61,7 @@ describe("TeamInfoView", () => {
|
|||
vi.mocked(networking.getGuardrailsList).mockResolvedValue([]);
|
||||
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
|
||||
|
||||
const { getByText } = render(
|
||||
render(
|
||||
<TeamInfoView
|
||||
teamId="123"
|
||||
onUpdate={() => {}}
|
||||
|
|
@ -75,7 +75,87 @@ describe("TeamInfoView", () => {
|
|||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(getByText("User ID")).toBeInTheDocument();
|
||||
expect(screen.queryByText("User ID")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not show all-proxy-models option when user has no access to it", async () => {
|
||||
vi.mocked(networking.teamInfoCall).mockResolvedValue({
|
||||
team_id: "123",
|
||||
team_info: {
|
||||
team_alias: "Test Team",
|
||||
team_id: "123",
|
||||
organization_id: null,
|
||||
admins: ["admin@test.com"],
|
||||
members: ["user1@test.com", "user2@test.com"],
|
||||
members_with_roles: [
|
||||
{
|
||||
user_id: "user1@test.com",
|
||||
user_email: "user1@test.com",
|
||||
role: "member",
|
||||
spend: 0,
|
||||
budget_id: "budget1",
|
||||
},
|
||||
],
|
||||
metadata: {},
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
max_budget: null,
|
||||
budget_duration: null,
|
||||
models: ["gpt-4"],
|
||||
blocked: false,
|
||||
spend: 0,
|
||||
max_parallel_requests: null,
|
||||
budget_reset_at: null,
|
||||
model_id: null,
|
||||
litellm_model_table: null,
|
||||
created_at: "2024-01-01T00:00:00Z",
|
||||
team_member_budget_table: null,
|
||||
},
|
||||
keys: [],
|
||||
team_memberships: [],
|
||||
});
|
||||
|
||||
vi.mocked(networking.getGuardrailsList).mockResolvedValue([]);
|
||||
vi.mocked(networking.fetchMCPAccessGroups).mockResolvedValue([]);
|
||||
|
||||
render(
|
||||
<TeamInfoView
|
||||
teamId="123"
|
||||
onUpdate={() => {}}
|
||||
onClose={() => {}}
|
||||
accessToken="123"
|
||||
is_team_admin={true}
|
||||
is_proxy_admin={true}
|
||||
userModels={["gpt-4", "gpt-3.5-turbo"]}
|
||||
editTeam={false}
|
||||
premiumUser={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText("Test Team")).not.toBeNull();
|
||||
});
|
||||
|
||||
const settingsTab = screen.getByRole("tab", { name: "Settings" });
|
||||
act(() => {
|
||||
fireEvent.click(settingsTab);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Team Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
act(() => {
|
||||
fireEvent.click(editButton);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Models")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const allProxyModelsOption = screen.queryByText("All Proxy Models");
|
||||
expect(allProxyModelsOption).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,50 +1,50 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import NumericalInput from "../shared/numerical_input";
|
||||
import UserSearchModal from "@/components/common_components/user_search_modal";
|
||||
import {
|
||||
Card,
|
||||
Title,
|
||||
Text,
|
||||
Tab,
|
||||
TabList,
|
||||
TabGroup,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Grid,
|
||||
Badge,
|
||||
Button as TremorButton,
|
||||
TextInput,
|
||||
} from "@tremor/react";
|
||||
import TeamMembersComponent from "./team_member_view";
|
||||
import MemberPermissions from "./member_permissions";
|
||||
import {
|
||||
teamInfoCall,
|
||||
teamMemberDeleteCall,
|
||||
teamMemberAddCall,
|
||||
teamMemberUpdateCall,
|
||||
Member,
|
||||
teamUpdateCall,
|
||||
getGuardrailsList,
|
||||
Member,
|
||||
teamInfoCall,
|
||||
teamMemberAddCall,
|
||||
teamMemberDeleteCall,
|
||||
teamMemberUpdateCall,
|
||||
teamUpdateCall,
|
||||
} from "@/components/networking";
|
||||
import { Button, Form, Input, Select, Switch, message, Tooltip } from "antd";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
|
||||
import { InfoCircleOutlined } from "@ant-design/icons";
|
||||
import { ArrowLeftIcon } from "@heroicons/react/outline";
|
||||
import MemberModal from "./edit_membership";
|
||||
import UserSearchModal from "@/components/common_components/user_search_modal";
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Grid,
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Button as TremorButton,
|
||||
} from "@tremor/react";
|
||||
import { Button, Form, Input, message, Select, Switch, Tooltip } from "antd";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
|
||||
import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key";
|
||||
import ObjectPermissionsView from "../object_permissions_view";
|
||||
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
|
||||
import LoggingSettingsView from "../logging_settings_view";
|
||||
import MCPServerSelector from "../mcp_server_management/MCPServerSelector";
|
||||
import MCPToolPermissions from "../mcp_server_management/MCPToolPermissions";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
import EditLoggingSettings from "./EditLoggingSettings";
|
||||
import LoggingSettingsView from "../logging_settings_view";
|
||||
import { fetchMCPAccessGroups } from "../networking";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils";
|
||||
import NotificationsManager from "../molecules/notifications_manager";
|
||||
import PassThroughRoutesSelector from "../common_components/PassThroughRoutesSelector";
|
||||
import { mapEmptyStringToNull } from "@/utils/keyUpdateUtils";
|
||||
import DeleteResourceModal from "../common_components/DeleteResourceModal";
|
||||
import { fetchMCPAccessGroups } from "../networking";
|
||||
import ObjectPermissionsView from "../object_permissions_view";
|
||||
import NumericalInput from "../shared/numerical_input";
|
||||
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
|
||||
import MemberModal from "./edit_membership";
|
||||
import EditLoggingSettings from "./EditLoggingSettings";
|
||||
import MemberPermissions from "./member_permissions";
|
||||
import TeamMembersComponent from "./team_member_view";
|
||||
|
||||
export interface TeamMembership {
|
||||
user_id: string;
|
||||
|
|
@ -586,11 +586,17 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
|
|||
<Input type="" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Models" name="models">
|
||||
<Form.Item
|
||||
label="Models"
|
||||
name="models"
|
||||
rules={[{ required: true, message: "Please select at least one model" }]}
|
||||
>
|
||||
<Select mode="multiple" placeholder="Select models">
|
||||
<Select.Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
</Select.Option>
|
||||
{(is_proxy_admin || userModels.includes("all-proxy-models")) && (
|
||||
<Select.Option key="all-proxy-models" value="all-proxy-models">
|
||||
All Proxy Models
|
||||
</Select.Option>
|
||||
)}
|
||||
<Select.Option key="no-default-models" value="no-default-models">
|
||||
No Default Models
|
||||
</Select.Option>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue