mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Merge eef4553e56 into 3746ba58d7
This commit is contained in:
commit
cdfdf5ddab
6 changed files with 133 additions and 4 deletions
|
|
@ -320,7 +320,11 @@ export const getKeyTableColumns = ({
|
|||
<SpendBudgetCell
|
||||
spend={row.original.spend}
|
||||
maxBudget={row.original.max_budget}
|
||||
inheritedGates={row.original.max_budget == null ? inheritedBudgetGates(team, organization) : []}
|
||||
inheritedGates={
|
||||
row.original.max_budget == null
|
||||
? inheritedBudgetGates(team, organization, row.original.team_id ? null : row.original.user)
|
||||
: []
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -111,6 +111,8 @@ export interface KeyResponse {
|
|||
user_id: string;
|
||||
user_email: string;
|
||||
user_alias: string | null;
|
||||
max_budget?: number | null;
|
||||
budget_duration?: string | null;
|
||||
};
|
||||
created_by_user?: {
|
||||
user_id: string;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,13 @@ const organization = {
|
|||
organization_alias: "Acme",
|
||||
litellm_budget_table: { max_budget: 5000, budget_duration: null },
|
||||
};
|
||||
const user = {
|
||||
user_id: "user-1",
|
||||
user_email: "owner@example.com",
|
||||
user_alias: "Key Owner",
|
||||
max_budget: 1500,
|
||||
budget_duration: "1mo",
|
||||
};
|
||||
|
||||
describe("inheritedBudgetGates", () => {
|
||||
it("returns team then org gates when both have budgets", () => {
|
||||
|
|
@ -42,6 +49,30 @@ describe("inheritedBudgetGates", () => {
|
|||
),
|
||||
).toEqual(["team-1", "org-1"]);
|
||||
});
|
||||
|
||||
it("returns the owner's user budget as a gate", () => {
|
||||
expect(inheritedBudgetGates(null, null, user)).toEqual([
|
||||
{ scope: "User", alias: "Key Owner", maxBudget: 1500, budgetDuration: "1mo" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips the user gate when the owner has no budget", () => {
|
||||
expect(inheritedBudgetGates(null, null, { ...user, max_budget: null })).toEqual([]);
|
||||
expect(inheritedBudgetGates(null, null, null)).toEqual([]);
|
||||
});
|
||||
|
||||
it("falls back to email then id for the user alias", () => {
|
||||
expect(inheritedBudgetGates(null, null, { ...user, user_alias: null })[0].alias).toBe("owner@example.com");
|
||||
expect(inheritedBudgetGates(null, null, { ...user, user_alias: null, user_email: null })[0].alias).toBe("user-1");
|
||||
});
|
||||
|
||||
it("lists team, org, and user gates together", () => {
|
||||
expect(inheritedBudgetGates(team, organization, user).map((g) => g.scope)).toEqual([
|
||||
"Team",
|
||||
"Organization",
|
||||
"User",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("InheritedBudgetHint", () => {
|
||||
|
|
@ -57,4 +88,10 @@ describe("InheritedBudgetHint", () => {
|
|||
expect(screen.getByTestId("inherited-budget-hint")).toHaveTextContent("Organization Acme: $5,000.00");
|
||||
expect(screen.getByTestId("inherited-budget-hint")).not.toHaveTextContent("Organization Acme: $5,000.00 /");
|
||||
});
|
||||
|
||||
it("shows the owner's user budget on hover", async () => {
|
||||
render(<InheritedBudgetHint gates={inheritedBudgetGates(null, null, user)} />);
|
||||
await userEvent.setup().hover(screen.getByLabelText("question-circle"));
|
||||
expect(screen.getByTestId("inherited-budget-hint")).toHaveTextContent("User Key Owner: $1,500.00 / 1mo");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { Organization } from "@/components/networking";
|
|||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
|
||||
export interface InheritedBudgetGate {
|
||||
scope: "Team" | "Organization";
|
||||
scope: "Team" | "Organization" | "User";
|
||||
alias: string;
|
||||
maxBudget: number;
|
||||
budgetDuration: string | null;
|
||||
|
|
@ -15,6 +15,14 @@ export interface InheritedBudgetGate {
|
|||
type TeamBudgetSource = Pick<Team, "team_id" | "team_alias" | "max_budget" | "budget_duration">;
|
||||
type OrganizationBudgetSource = Pick<Organization, "organization_id" | "organization_alias" | "litellm_budget_table">;
|
||||
|
||||
export interface UserBudgetSource {
|
||||
user_id: string;
|
||||
user_alias?: string | null;
|
||||
user_email?: string | null;
|
||||
max_budget?: number | null;
|
||||
budget_duration?: string | null;
|
||||
}
|
||||
|
||||
const teamGate = (team: TeamBudgetSource | null | undefined): InheritedBudgetGate | null =>
|
||||
team && team.max_budget != null
|
||||
? {
|
||||
|
|
@ -38,10 +46,22 @@ const organizationGate = (organization: OrganizationBudgetSource | null | undefi
|
|||
: null;
|
||||
};
|
||||
|
||||
const userGate = (user: UserBudgetSource | null | undefined): InheritedBudgetGate | null =>
|
||||
user && user.max_budget != null
|
||||
? {
|
||||
scope: "User",
|
||||
alias: user.user_alias || user.user_email || user.user_id,
|
||||
maxBudget: user.max_budget,
|
||||
budgetDuration: user.budget_duration ?? null,
|
||||
}
|
||||
: null;
|
||||
|
||||
export const inheritedBudgetGates = (
|
||||
team: TeamBudgetSource | null | undefined,
|
||||
organization: OrganizationBudgetSource | null | undefined,
|
||||
): readonly InheritedBudgetGate[] => [teamGate(team), organizationGate(organization)].filter((gate) => gate !== null);
|
||||
user?: UserBudgetSource | null,
|
||||
): readonly InheritedBudgetGate[] =>
|
||||
[teamGate(team), organizationGate(organization), userGate(user)].filter((gate) => gate !== null);
|
||||
|
||||
const formatGate = (gate: InheritedBudgetGate): string =>
|
||||
`${gate.scope} ${gate.alias}: $${formatNumberWithCommas(gate.maxBudget, 2)}${gate.budgetDuration ? ` / ${gate.budgetDuration}` : ""}`;
|
||||
|
|
|
|||
|
|
@ -258,6 +258,71 @@ describe("KeyInfoView overview budget display (LIT-2845)", () => {
|
|||
expect(screen.getByTestId("inherited-budget-hint")).not.toHaveTextContent("Team Org Team");
|
||||
});
|
||||
|
||||
it("lists the owner's user budget in the hint for a personal key with no budget of its own", async () => {
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={
|
||||
{
|
||||
...MOCK_KEY_DATA,
|
||||
max_budget: null,
|
||||
team_id: null,
|
||||
user: {
|
||||
user_id: "user-1",
|
||||
user_email: "owner@example.com",
|
||||
user_alias: "Budget Owner",
|
||||
max_budget: 1500,
|
||||
budget_duration: "1mo",
|
||||
},
|
||||
} as unknown as KeyResponse
|
||||
}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/of Unlimited/)).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.setup().hover(screen.getByLabelText("question-circle"));
|
||||
expect(screen.getByTestId("inherited-budget-hint")).toHaveTextContent("User Budget Owner: $1,500.00 / 1mo");
|
||||
});
|
||||
|
||||
it("omits the owner's user budget from the hint for a team key", async () => {
|
||||
vi.mocked(useTeams).mockReturnValue({
|
||||
teams: [makeTeam({ team_id: "team-123", team_alias: "Test Budget", max_budget: 1200, budget_duration: "30d" })],
|
||||
setTeams: vi.fn(),
|
||||
});
|
||||
renderWithProviders(
|
||||
<KeyInfoView
|
||||
keyData={
|
||||
{
|
||||
...MOCK_KEY_DATA,
|
||||
max_budget: null,
|
||||
team_id: "team-123",
|
||||
user: {
|
||||
user_id: "user-1",
|
||||
user_email: "owner@example.com",
|
||||
user_alias: "Budget Owner",
|
||||
max_budget: 1500,
|
||||
budget_duration: "1mo",
|
||||
},
|
||||
} as unknown as KeyResponse
|
||||
}
|
||||
onClose={() => {}}
|
||||
keyId={"test-key-id"}
|
||||
onKeyDataUpdate={() => {}}
|
||||
teams={[]}
|
||||
/>,
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/of Unlimited/)).toBeInTheDocument();
|
||||
});
|
||||
await userEvent.setup().hover(screen.getByLabelText("question-circle"));
|
||||
expect(screen.getByTestId("inherited-budget-hint")).toHaveTextContent("Team Test Budget: $1,200.00 / 30d");
|
||||
expect(screen.getByTestId("inherited-budget-hint")).not.toHaveTextContent("User Budget Owner");
|
||||
});
|
||||
|
||||
it("renders 'Unlimited' with no hint when neither key, team, nor org has a budget", async () => {
|
||||
vi.mocked(useTeams).mockReturnValue({
|
||||
teams: [makeTeam({ team_id: "team-789", team_alias: "Free Team" })],
|
||||
|
|
|
|||
|
|
@ -472,7 +472,8 @@ export default function KeyInfoView({
|
|||
|
||||
const hasOwnBudget = currentKeyData.max_budget !== null;
|
||||
const budgetDisplay = hasOwnBudget ? `$${formatNumberWithCommas(currentKeyData.max_budget, 2)}` : "Unlimited";
|
||||
const inheritedGates = hasOwnBudget ? [] : inheritedBudgetGates(parentTeam, parentOrg);
|
||||
const ownerUser = currentKeyData.team_id ? null : currentKeyData.user;
|
||||
const inheritedGates = hasOwnBudget ? [] : inheritedBudgetGates(parentTeam, parentOrg, ownerUser);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full overflow-y-auto p-4">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue