mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(ui): state each key budget's real threshold in the Budgets tab
Scopes disagree on whether hitting the limit exactly is over it. team_member enforces >= so 50 of 50 is already denied, while team enforces > so 300 of 300 still passes. The table rendered the numbers and the server's status but not the operator, so two rows could show identical spend and limit with opposite statuses and nothing on screen explaining why. Each row with a limit now states its own threshold, "Blocks at >= $300.00" against "Blocks at > $300.00", and a soft row says "Alerts at" so the wording never promises a block it cannot make.
This commit is contained in:
parent
d77cb4a427
commit
3b5ab06a95
3 changed files with 182 additions and 3 deletions
|
|
@ -0,0 +1,112 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { KeyBudgetEntry } from "@/app/(dashboard)/hooks/keys/useKeyBudgets";
|
||||
import { budgetThresholdRule, isBlockingRow, severityRank } from "./KeyBudgetsTableColumns";
|
||||
|
||||
const UNCONFIGURED_BUDGET = {
|
||||
scope: "key",
|
||||
entity_type: "key",
|
||||
entity_id: null,
|
||||
entity_label: null,
|
||||
enforcement: "hard",
|
||||
max_budget: null,
|
||||
spend: 0,
|
||||
remaining: null,
|
||||
comparison: ">=",
|
||||
budget_duration: null,
|
||||
budget_reset_at: null,
|
||||
window_start: null,
|
||||
source: "key.max_budget",
|
||||
status: "unlimited",
|
||||
note: null,
|
||||
} as KeyBudgetEntry;
|
||||
|
||||
const TEAM_MEMBER_AT_LIMIT: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
scope: "team_member",
|
||||
comparison: ">=",
|
||||
max_budget: 50,
|
||||
spend: 50,
|
||||
remaining: 0,
|
||||
status: "exceeded",
|
||||
};
|
||||
|
||||
const TEAM_UNDER_LIMIT: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
scope: "team",
|
||||
comparison: ">",
|
||||
max_budget: 300,
|
||||
spend: 120,
|
||||
remaining: 180,
|
||||
status: "ok",
|
||||
};
|
||||
|
||||
const INCLUSIVE_AT_300: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
comparison: ">=",
|
||||
max_budget: 300,
|
||||
spend: 300,
|
||||
remaining: 0,
|
||||
status: "exceeded",
|
||||
};
|
||||
|
||||
const EXCLUSIVE_AT_300: KeyBudgetEntry = { ...INCLUSIVE_AT_300, comparison: ">", status: "ok" };
|
||||
|
||||
const SOFT_OVER: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
enforcement: "soft",
|
||||
comparison: ">=",
|
||||
max_budget: 500,
|
||||
spend: 900,
|
||||
remaining: -400,
|
||||
status: "exceeded",
|
||||
};
|
||||
|
||||
const SUB_DOLLAR_LIMIT: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, comparison: ">", max_budget: 0.1 };
|
||||
|
||||
const BLOCKING: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, status: "exceeded", enforcement: "hard" };
|
||||
const ALERT_ONLY: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, status: "exceeded", enforcement: "soft" };
|
||||
const HEALTHY: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, status: "ok", enforcement: "hard" };
|
||||
|
||||
describe("budgetThresholdRule", () => {
|
||||
it("marks an inclusive scope as blocking at the limit, so spend equal to it is already denied", () => {
|
||||
expect(budgetThresholdRule(TEAM_MEMBER_AT_LIMIT)).toBe("Blocks at ≥ $50.00");
|
||||
});
|
||||
|
||||
it("marks an exclusive scope as blocking only above the limit", () => {
|
||||
expect(budgetThresholdRule(TEAM_UNDER_LIMIT)).toBe("Blocks at > $300.00");
|
||||
});
|
||||
|
||||
it("distinguishes two scopes sitting on identical numbers by their operator alone", () => {
|
||||
expect(budgetThresholdRule(INCLUSIVE_AT_300)).toBe("Blocks at ≥ $300.00");
|
||||
expect(budgetThresholdRule(EXCLUSIVE_AT_300)).toBe("Blocks at > $300.00");
|
||||
});
|
||||
|
||||
it("never promises a soft budget will block", () => {
|
||||
expect(budgetThresholdRule(SOFT_OVER)).toBe("Alerts at ≥ $500.00");
|
||||
});
|
||||
|
||||
it("states no threshold for a scope with nothing configured", () => {
|
||||
expect(budgetThresholdRule(UNCONFIGURED_BUDGET)).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps sub-dollar limits legible rather than rounding them to zero", () => {
|
||||
expect(budgetThresholdRule(SUB_DOLLAR_LIMIT)).toBe("Blocks at > $0.10");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isBlockingRow", () => {
|
||||
it("counts only a hard budget that is over as blocking", () => {
|
||||
expect(isBlockingRow(BLOCKING)).toBe(true);
|
||||
expect(isBlockingRow(ALERT_ONLY)).toBe(false);
|
||||
expect(isBlockingRow(HEALTHY)).toBe(false);
|
||||
expect(isBlockingRow(UNCONFIGURED_BUDGET)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("severityRank", () => {
|
||||
it("ranks a blocking budget above an alert-only one, and both above healthy and unlimited", () => {
|
||||
expect(severityRank(BLOCKING)).toBeLessThan(severityRank(ALERT_ONLY));
|
||||
expect(severityRank(ALERT_ONLY)).toBeLessThan(severityRank(HEALTHY));
|
||||
expect(severityRank(HEALTHY)).toBeLessThan(severityRank(UNCONFIGURED_BUDGET));
|
||||
});
|
||||
});
|
||||
|
|
@ -11,6 +11,7 @@ import {
|
|||
StatusBadge,
|
||||
type StatusTone,
|
||||
} from "@/components/shared/table_cells";
|
||||
import { formatNumberWithCommas } from "@/utils/dataUtils";
|
||||
|
||||
const SCOPE_LABELS: Record<string, string> = {
|
||||
proxy: "Proxy",
|
||||
|
|
@ -37,6 +38,19 @@ const STATUS_ORDER: Record<string, number> = { exceeded: 0, ok: 1, unlimited: 2
|
|||
export const severityRank = (entry: KeyBudgetEntry): number =>
|
||||
(STATUS_ORDER[entry.status] ?? STATUS_ORDER.unlimited) * 2 + (isAlertOnly(entry) ? 1 : 0);
|
||||
|
||||
const COMPARISON_GLYPH: Record<string, string> = { ">=": "≥", ">": ">" };
|
||||
|
||||
/**
|
||||
* Scopes disagree on whether hitting the limit exactly is over it: team_member enforces `>=`
|
||||
* so 50 of 50 is already denied, while team enforces `>` so 300 of 300 still passes. Two rows
|
||||
* can therefore show identical numbers and opposite statuses, so state each row's real threshold.
|
||||
*/
|
||||
export const budgetThresholdRule = (entry: KeyBudgetEntry): string | null => {
|
||||
if (entry.max_budget == null) return null;
|
||||
const threshold = `${COMPARISON_GLYPH[entry.comparison] ?? entry.comparison} $${formatNumberWithCommas(entry.max_budget, 2)}`;
|
||||
return isAlertOnly(entry) ? `Alerts at ${threshold}` : `Blocks at ${threshold}`;
|
||||
};
|
||||
|
||||
const statusPresentation = (entry: KeyBudgetEntry): { tone: StatusTone; label: string } => {
|
||||
if (entry.status === "unlimited") return { tone: "neutral", label: "Unlimited" };
|
||||
if (entry.status !== "exceeded") return { tone: "success", label: "Within budget" };
|
||||
|
|
@ -114,9 +128,15 @@ export const getKeyBudgetsTableColumns = (): ColumnDef<KeyBudgetEntry>[] => [
|
|||
header: "Spend / Limit",
|
||||
size: 200,
|
||||
enableSorting: false,
|
||||
cell: ({ row }) => (
|
||||
<SpendBudgetCell spend={row.original.spend} maxBudget={row.original.max_budget} budgetDecimals={2} />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const rule = budgetThresholdRule(row.original);
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<SpendBudgetCell spend={row.original.spend} maxBudget={row.original.max_budget} budgetDecimals={2} />
|
||||
{rule && <span className="whitespace-nowrap text-xs tabular-nums text-muted-foreground">{rule}</span>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "remaining",
|
||||
|
|
@ -138,6 +158,7 @@ export const getKeyBudgetsTableColumns = (): ColumnDef<KeyBudgetEntry>[] => [
|
|||
<StatusBadge
|
||||
tone={tone}
|
||||
label={label}
|
||||
tooltip={budgetThresholdRule(row.original)}
|
||||
dataTestId={isBlockingRow(row.original) ? "key-budget-blocking" : undefined}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -271,6 +271,52 @@ describe("KeyInfoView Budgets tab", () => {
|
|||
expect(softRow).toHaveTextContent("alert only");
|
||||
});
|
||||
|
||||
it("explains why two rows on identical numbers get opposite statuses", async () => {
|
||||
const inclusive: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
scope: "team_member",
|
||||
entity_type: "team_member",
|
||||
entity_label: "alice @ Platform",
|
||||
comparison: ">=",
|
||||
max_budget: 300,
|
||||
spend: 300,
|
||||
remaining: 0,
|
||||
status: "exceeded",
|
||||
};
|
||||
const exclusive: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
scope: "team",
|
||||
entity_type: "team",
|
||||
entity_label: "Platform",
|
||||
comparison: ">",
|
||||
max_budget: 300,
|
||||
spend: 300,
|
||||
remaining: 0,
|
||||
status: "ok",
|
||||
};
|
||||
mockBudgets([inclusive, exclusive]);
|
||||
const panel = await renderAndOpenBudgetsTab();
|
||||
|
||||
const blockedRow = rowFor(panel, "alice @ Platform");
|
||||
const allowedRow = rowFor(panel, "Platform");
|
||||
|
||||
expect(blockedRow).toHaveTextContent("Blocks at ≥ $300.00");
|
||||
expect(allowedRow).toHaveTextContent("Blocks at > $300.00");
|
||||
|
||||
expect(within(blockedRow).getByTestId("key-budget-blocking")).toBeInTheDocument();
|
||||
expect(within(allowedRow).queryByTestId("key-budget-blocking")).not.toBeInTheDocument();
|
||||
expect(within(allowedRow).getByText("Within budget")).toBeInTheDocument();
|
||||
|
||||
expect(blockedRow).toHaveTextContent("$300.0000 of $300.00");
|
||||
expect(allowedRow).toHaveTextContent("$300.0000 of $300.00");
|
||||
});
|
||||
|
||||
it("states no threshold for a scope with nothing configured", async () => {
|
||||
const panel = await renderAndOpenBudgetsTab();
|
||||
|
||||
expect(rowFor(panel, "Acme Org")).not.toHaveTextContent("Blocks at");
|
||||
});
|
||||
|
||||
it("renders a scope with nothing configured as Unlimited rather than $0", async () => {
|
||||
const panel = await renderAndOpenBudgetsTab();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue