Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,20 @@ describe("OrganizationsTable", () => {
expect(screen.getByText("RPM: Unlimited")).toBeInTheDocument();
});

it("renders a tpm/rpm limit of 0 as 0, never as Unlimited", () => {
render(
<OrganizationsTable
{...baseProps}
organizations={[makeOrganization({ litellm_budget_table: { max_budget: null, tpm_limit: 0, rpm_limit: 0 } })]}
/>,
);

expect(screen.getByText("TPM: 0")).toBeInTheDocument();
expect(screen.getByText("RPM: 0")).toBeInTheDocument();
expect(screen.queryByText("TPM: Unlimited")).not.toBeInTheDocument();
expect(screen.queryByText("RPM: Unlimited")).not.toBeInTheDocument();
});

it("renders loading skeletons instead of rows while loading", () => {
render(
<OrganizationsTable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ function OrganizationLimitsCell({ organization }: { organization: Organization }
const { tpm_limit, rpm_limit } = getOrganizationBudget(organization);
return (
<div className="flex flex-col text-xs text-muted-foreground">
<span>TPM: {tpm_limit ? tpm_limit : "Unlimited"}</span>
<span>RPM: {rpm_limit ? rpm_limit : "Unlimited"}</span>
<span>TPM: {tpm_limit ?? "Unlimited"}</span>
<span>RPM: {rpm_limit ?? "Unlimited"}</span>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { vi, test, expect, beforeEach } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
Expand Down Expand Up @@ -106,7 +106,7 @@
});

test("renders organization view after loading data", async () => {
mockUseOrganization.mockReturnValue({ data: mockOrg, isLoading: false } as any);

Check warning on line 109 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { findAllByText } = renderWithProviders(
<OrganizationInfoView
Expand All @@ -125,7 +125,7 @@
});

test("should display empty state when organization has no members", async () => {
mockUseOrganization.mockReturnValue({ data: mockOrg, isLoading: false } as any);

Check warning on line 128 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const user = userEvent.setup();
renderWithProviders(
Expand Down Expand Up @@ -156,7 +156,7 @@
...mockOrg,
teams: [{ team_id: "team_123" }, { team_id: "team_456" }],
};
mockUseOrganization.mockReturnValue({ data: orgWithTeams, isLoading: false } as any);

Check warning on line 159 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

renderWithProviders(
<OrganizationInfoView
Expand Down Expand Up @@ -190,7 +190,7 @@
...mockOrg,
teams: [{ team_id: "team_999" }],
};
mockUseOrganization.mockReturnValue({ data: orgWithUnknownTeam, isLoading: false } as any);

Check warning on line 193 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

renderWithProviders(
<OrganizationInfoView
Expand All @@ -214,7 +214,7 @@
...mockOrg,
teams: [{ team_id: "team_123" }, { team_id: "team_456" }],
};
mockUseOrganization.mockReturnValue({ data: orgWithTeams, isLoading: false } as any);

Check warning on line 217 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

renderWithProviders(
<OrganizationInfoView
Expand All @@ -241,7 +241,7 @@
});

test("model badges stay non-clickable", async () => {
mockUseOrganization.mockReturnValue({ data: mockOrg, isLoading: false } as any);

Check warning on line 244 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

renderWithProviders(
<OrganizationInfoView
Expand All @@ -262,7 +262,7 @@
});

test("should keep unsaved settings edits when switching tabs and back", async () => {
mockUseOrganization.mockReturnValue({ data: mockOrg, isLoading: false } as any);

Check warning on line 265 in ui/litellm-dashboard/src/components/organization/organization_view.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const user = userEvent.setup();
renderWithProviders(
Expand Down Expand Up @@ -290,3 +290,37 @@

expect(screen.getByLabelText(/Organization Name/i)).toHaveValue("Renamed Org");
});

test("renders a tpm/rpm limit of 0 as 0 in the overview and settings tabs, never as Unlimited", async () => {
const zeroLimitOrg = {
...mockOrg,
litellm_budget_table: { ...mockOrg.litellm_budget_table, tpm_limit: 0, rpm_limit: 0 },
};
mockUseOrganization.mockReturnValue({ data: zeroLimitOrg, isLoading: false } as unknown as ReturnType<
typeof useOrganization
>);

const user = userEvent.setup();
renderWithProviders(
<OrganizationInfoView
organizationId="org_123"
onClose={() => {}}
accessToken="test-token"
is_org_admin={false}
is_proxy_admin={true}
userModels={[]}
editOrg={false}
/>,
);

const overview = await screen.findByRole("tabpanel", { name: "Overview" });
expect(within(overview).getByText("TPM: 0")).toBeInTheDocument();
expect(within(overview).getByText("RPM: 0")).toBeInTheDocument();

await user.click(screen.getByRole("tab", { name: "Settings" }));
const settings = await screen.findByRole("tabpanel", { name: "Settings" });
expect(within(settings).getByText("TPM: 0")).toBeInTheDocument();
expect(within(settings).getByText("RPM: 0")).toBeInTheDocument();
expect(screen.queryByText("TPM: Unlimited")).not.toBeInTheDocument();
expect(screen.queryByText("RPM: Unlimited")).not.toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
is_proxy_admin,
userModels,
editOrg,
}) => {

Check warning on line 47 in ui/litellm-dashboard/src/components/organization/organization_view.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Arrow function has a complexity of 22. Maximum allowed is 20
const queryClient = useQueryClient();
const { data: orgData, isLoading: loading } = useOrganization(organizationId);
const [isEditing, setIsEditing] = useState(false);
Expand All @@ -57,7 +57,7 @@

const teamAliasMap = useMemo(() => createTeamAliasMap(teams), [teams]);

const handleMemberAdd = async (values: any) => {

Check warning on line 60 in ui/litellm-dashboard/src/components/organization/organization_view.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
try {
if (accessToken == null) {
return;
Expand All @@ -79,7 +79,7 @@
}
};

const handleMemberUpdate = async (values: any) => {

Check warning on line 82 in ui/litellm-dashboard/src/components/organization/organization_view.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
try {
if (!accessToken) return;

Expand Down Expand Up @@ -206,8 +206,8 @@
<CardContent>
<p className="text-sm text-muted-foreground">Rate Limits</p>
<div className="mt-2 text-sm text-foreground">
<p>TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}</p>
<p>RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}</p>
<p>TPM: {orgData.litellm_budget_table.tpm_limit ?? "Unlimited"}</p>
<p>RPM: {orgData.litellm_budget_table.rpm_limit ?? "Unlimited"}</p>
{orgData.litellm_budget_table.max_parallel_requests && (
<p>Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests}</p>
)}
Expand Down Expand Up @@ -311,8 +311,8 @@
</div>
<div>
<p className="font-medium text-foreground">Rate Limits</p>
<div>TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}</div>
<div>RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}</div>
<div>TPM: {orgData.litellm_budget_table.tpm_limit ?? "Unlimited"}</div>
<div>RPM: {orgData.litellm_budget_table.rpm_limit ?? "Unlimited"}</div>
</div>
<div>
<p className="font-medium text-foreground">Budget</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe("EditMembership submit payload", () => {
expect(submitted()).toStrictEqual(expected);
});

it("collapses falsy budget and limit values to null and a missing model list to an empty array", async () => {
it("keeps stored 0 budget and limits as 0 on an untouched save, collapsing only empty strings and a missing model list", async () => {
renderEdit(teamMemberConfig, {
user_id: "u1",
user_email: "a@b.com",
Expand All @@ -128,10 +128,10 @@ describe("EditMembership submit payload", () => {
user_email: "a@b.com",
user_id: "u1",
role: "user",
max_budget_in_team: null,
max_budget_in_team: 0,
budget_duration: null,
tpm_limit: null,
rpm_limit: null,
tpm_limit: 0,
rpm_limit: 0,
allowed_models: [],
});
});
Expand Down
28 changes: 28 additions & 0 deletions ui/litellm-dashboard/src/components/team/TeamInfo.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,34 @@ describe("TeamInfoView", () => {
expect(screen.getByText(/of \$1,000\.00/)).toBeInTheDocument();
});

it("renders a tpm/rpm/budget limit of 0 as 0 in the overview and settings tabs, never as Unlimited or No Limit", async () => {
vi.mocked(networking.teamInfoCall).mockResolvedValue(
createMockTeamData({
tpm_limit: 0,
rpm_limit: 0,
team_member_budget_table: { max_budget: 0, budget_duration: null, tpm_limit: 0, rpm_limit: 0 },
}),
);

renderWithProviders(<TeamInfoView {...defaultProps} />);

const overview = await screen.findByRole("tabpanel", { name: "Overview" });
expect(within(overview).getByText("TPM: 0")).toBeInTheDocument();
expect(within(overview).getByText("RPM: 0")).toBeInTheDocument();

await userEvent.setup({ delay: null }).click(screen.getByRole("tab", { name: "Settings" }));
const settings = await screen.findByRole("tabpanel", { name: "Settings" });
expect(within(settings).getByText("TPM: 0")).toBeInTheDocument();
expect(within(settings).getByText("RPM: 0")).toBeInTheDocument();
expect(within(settings).getByText("TPM Limit: 0")).toBeInTheDocument();
expect(within(settings).getByText("RPM Limit: 0")).toBeInTheDocument();
expect(within(settings).getByText("Max Budget: 0")).toBeInTheDocument();
expect(screen.queryByText("TPM: Unlimited")).not.toBeInTheDocument();
expect(screen.queryByText("RPM: Unlimited")).not.toBeInTheDocument();
expect(screen.queryByText("TPM Limit: No Limit")).not.toBeInTheDocument();
expect(screen.queryByText("RPM Limit: No Limit")).not.toBeInTheDocument();
});

it("should display guardrails in overview when present", async () => {
vi.mocked(networking.teamInfoCall).mockResolvedValue(
createMockTeamData({
Expand Down
14 changes: 7 additions & 7 deletions ui/litellm-dashboard/src/components/team/TeamInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -971,8 +971,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Card className="block p-6">
<p>Rate Limits</p>
<div className="mt-2">
<p>TPM: {info.tpm_limit || "Unlimited"}</p>
<p>RPM: {info.rpm_limit || "Unlimited"}</p>
<p>TPM: {info.tpm_limit ?? "Unlimited"}</p>
<p>RPM: {info.rpm_limit ?? "Unlimited"}</p>
{info.max_parallel_requests && <p>Max Parallel Requests: {info.max_parallel_requests}</p>}
{(() => {
const modelTpm = (info.metadata?.model_tpm_limit ?? {}) as Record<string, number>;
Expand Down Expand Up @@ -1760,8 +1760,8 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
</div>
<div>
<p className="font-medium">Rate Limits</p>
<div>TPM: {info.tpm_limit || "Unlimited"}</div>
<div>RPM: {info.rpm_limit || "Unlimited"}</div>
<div>TPM: {info.tpm_limit ?? "Unlimited"}</div>
<div>RPM: {info.rpm_limit ?? "Unlimited"}</div>
{(() => {
const modelTpm = (info.metadata?.model_tpm_limit ?? {}) as Record<string, number>;
const modelRpm = (info.metadata?.model_rpm_limit ?? {}) as Record<string, number>;
Expand Down Expand Up @@ -1811,11 +1811,11 @@ const TeamInfoView: React.FC<TeamInfoProps> = ({
<Info className="ml-1 inline size-3.5 align-text-bottom" />
</SimpleTooltip>
</p>
<div>Max Budget: {info.team_member_budget_table?.max_budget || "No Limit"}</div>
<div>Max Budget: {info.team_member_budget_table?.max_budget ?? "No Limit"}</div>
<div>Budget Duration: {info.team_member_budget_table?.budget_duration || "No Limit"}</div>
<div>Key Duration: {info.metadata?.team_member_key_duration || "No Limit"}</div>
<div>TPM Limit: {info.team_member_budget_table?.tpm_limit || "No Limit"}</div>
<div>RPM Limit: {info.team_member_budget_table?.rpm_limit || "No Limit"}</div>
<div>TPM Limit: {info.team_member_budget_table?.tpm_limit ?? "No Limit"}</div>
<div>RPM Limit: {info.team_member_budget_table?.rpm_limit ?? "No Limit"}</div>
</div>
<div>
<p className="font-medium">Router Settings</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
Expand Down Expand Up @@ -322,6 +322,43 @@ describe("TeamMembersComponent", () => {
expect(mockSetSelectedEditMember).toHaveBeenCalled();
});

it("keeps a member's stored 0 limits as 0 in the table and in the edit payload, never unlimited", async () => {
const user = userEvent.setup();
vi.mocked(isProxyAdminRole).mockReturnValue(true);
const baseTeamData = createMockTeamData();
const teamData = {
...baseTeamData,
team_memberships: baseTeamData.team_memberships.map((membership, index) =>
index === 0
? {
...membership,
litellm_budget_table: { ...membership.litellm_budget_table, max_budget: 0, tpm_limit: 0, rpm_limit: 0 },
}
: membership,
),
};

renderWithProviders(
<TeamMembersComponent
teamData={teamData}
canEditTeam={true}
handleMemberDelete={mockHandleMemberDelete}
setSelectedEditMember={mockSetSelectedEditMember}
setIsEditMemberModalVisible={mockSetIsEditMemberModalVisible}
setIsAddMemberModalVisible={mockSetIsAddMemberModalVisible}
/>,
);

const memberRow = screen.getByRole("row", { name: /user1@test\.com/ });
expect(within(memberRow).getByText("0 RPM / 0 TPM")).toBeInTheDocument();
expect(within(memberRow).queryByText("No Limits")).not.toBeInTheDocument();

await user.click(within(memberRow).getByTestId("edit-member"));

const zeroLimitsMember = { user_id: "user1@test.com", max_budget_in_team: 0, tpm_limit: 0, rpm_limit: 0 };
expect(mockSetSelectedEditMember).toHaveBeenCalledWith(expect.objectContaining(zeroLimitsMember));
});

it("should call setIsAddMemberModalVisible when Add Member button is clicked", async () => {
const user = userEvent.setup();

Expand Down
10 changes: 5 additions & 5 deletions ui/litellm-dashboard/src/components/team/TeamMemberTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export default function TeamMemberTab({
const rpmLimit = membership?.litellm_budget_table?.rpm_limit;
const tpmLimit = membership?.litellm_budget_table?.tpm_limit;

const rpmText = rpmLimit ? `${formatNumber(rpmLimit)} RPM` : null;
const tpmText = tpmLimit ? `${formatNumber(tpmLimit)} TPM` : null;
const rpmText = rpmLimit != null ? `${formatNumber(rpmLimit)} RPM` : null;
const tpmText = tpmLimit != null ? `${formatNumber(tpmLimit)} TPM` : null;

const limits = [rpmText, tpmText].filter(Boolean);
return limits.length > 0 ? limits.join(" / ") : "No Limits";
Expand Down Expand Up @@ -191,9 +191,9 @@ export default function TeamMemberTab({
const membership = teamData.team_memberships.find((tm) => tm.user_id === record.user_id);
const enhancedMember = {
...record,
max_budget_in_team: membership?.litellm_budget_table?.max_budget || null,
tpm_limit: membership?.litellm_budget_table?.tpm_limit || null,
rpm_limit: membership?.litellm_budget_table?.rpm_limit || null,
max_budget_in_team: membership?.litellm_budget_table?.max_budget ?? null,
tpm_limit: membership?.litellm_budget_table?.tpm_limit ?? null,
rpm_limit: membership?.litellm_budget_table?.rpm_limit ?? null,
budget_duration: membership?.litellm_budget_table?.budget_duration || null,
allowed_models: membership?.litellm_budget_table?.allowed_models || [],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,27 @@ describe("buildMemberFormValues", () => {
});
});

it("collapses falsy budgets and limits to null and a missing model list to an empty array", () => {
it("keeps a stored budget or limit of 0 as 0 because only null means unlimited", () => {
expect(
buildMemberFormValues(
"edit",
{ user_email: "a@b.com", user_id: "u1", role: "user", max_budget_in_team: 0, tpm_limit: 0, rpm_limit: 0 },
teamConfig,
),
).toStrictEqual({
user_email: "a@b.com",
user_id: "u1",
role: "user",
max_budget_in_team: 0,
budget_duration: null,
tpm_limit: 0,
rpm_limit: 0,
allowed_models: [],
});
});

it("collapses missing budgets and limits to null and a missing model list to an empty array", () => {
const unlimitedMember = {
user_email: "a@b.com",
user_id: "u1",
role: "user",
Expand All @@ -98,7 +111,10 @@ describe("buildMemberFormValues", () => {
tpm_limit: null,
rpm_limit: null,
allowed_models: [],
});
};
expect(
buildMemberFormValues("edit", { user_email: "a@b.com", user_id: "u1", role: "user" }, teamConfig),
).toStrictEqual(unlimitedMember);
});

it("falls back to the configured default role when the member has none", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ export const buildMemberFormValues = (
const seeded: MemberFormValues = {
...initialData,
role: (initialData.role as string) || config.defaultRole,
max_budget_in_team: initialData.max_budget_in_team || null,
tpm_limit: initialData.tpm_limit || null,
rpm_limit: initialData.rpm_limit || null,
max_budget_in_team: initialData.max_budget_in_team ?? null,
tpm_limit: initialData.tpm_limit ?? null,
rpm_limit: initialData.rpm_limit ?? null,
budget_duration: initialData.budget_duration || null,
allowed_models: initialData.allowed_models || [],
};
Expand Down
Loading