mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(ui): stop rendering an unreadable budget spend as a healthy $0.00
SpendBudgetCell coerces a null spend to 0 and draws an empty meter, so a budget whose live counter could not be read rendered as untouched headroom. The key budgets table now branches before that: an unreadable spend reads "Unknown" against its limit with no meter, which is the difference between "we could not tell you" and "there is nothing to tell".
This commit is contained in:
parent
33b519e014
commit
1cbf830083
3 changed files with 80 additions and 10 deletions
|
|
@ -132,6 +132,34 @@ BudgetComparison = Literal[">=", ">"]
|
|||
|
||||
BudgetStatus = Literal["unlimited", "ok", "exceeded"]
|
||||
|
||||
BudgetNoteCode = Literal[
|
||||
"alert_only",
|
||||
"custom_auth_may_override_end_user_cap",
|
||||
"end_user_route_only",
|
||||
"model_budget_fails_open",
|
||||
"per_model_counters",
|
||||
"project_spend_not_tracked",
|
||||
"request_tags_add_budgets",
|
||||
"reservation_blocks_at_limit",
|
||||
"rolling_window",
|
||||
"throttled_instead_of_blocked",
|
||||
"user_budget_not_applied_to_team_key",
|
||||
]
|
||||
|
||||
BudgetNoteSeverity = Literal["info", "warning"]
|
||||
|
||||
BudgetSpendState = Literal["live", "no_counter", "unavailable"]
|
||||
|
||||
|
||||
class KeyBudgetNote(BaseModel):
|
||||
"""One caveat about a budget row. Branch on ``code``; ``text`` is free to be reworded."""
|
||||
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
code: BudgetNoteCode
|
||||
severity: BudgetNoteSeverity
|
||||
text: str
|
||||
|
||||
|
||||
class KeyBudgetEntry(BaseModel):
|
||||
"""One budget that can gate requests made with a key, with its live spend."""
|
||||
|
|
@ -143,6 +171,7 @@ class KeyBudgetEntry(BaseModel):
|
|||
enforcement: BudgetEnforcement
|
||||
max_budget: float | None = None
|
||||
spend: float | None = None
|
||||
spend_state: BudgetSpendState
|
||||
remaining: float | None = None
|
||||
comparison: BudgetComparison
|
||||
budget_duration: str | None = None
|
||||
|
|
@ -150,7 +179,7 @@ class KeyBudgetEntry(BaseModel):
|
|||
window_start: datetime | None = None
|
||||
source: str
|
||||
status: BudgetStatus
|
||||
note: str | None = None
|
||||
notes: tuple[KeyBudgetNote, ...] = ()
|
||||
|
||||
|
||||
class KeyBudgetsResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -94,6 +94,30 @@ function EnforcementCell({ entry }: { entry: KeyBudgetEntry }) {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A null spend means the live counter could not be read, which `SpendBudgetCell` coerces to 0 and
|
||||
* draws as an empty meter. That renders a failed read as confident full headroom, so branch before
|
||||
* reaching it: an unknown number must not look like a healthy one.
|
||||
*/
|
||||
function SpendCell({ entry }: { entry: KeyBudgetEntry }) {
|
||||
const rule = budgetThresholdRule(entry);
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{entry.spend == null ? (
|
||||
<span className="whitespace-nowrap text-xs">
|
||||
<span className="font-medium text-amber-600">Unknown</span>{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{entry.max_budget == null ? "· Unlimited" : `of $${formatNumberWithCommas(entry.max_budget, 2)}`}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<SpendBudgetCell spend={entry.spend} maxBudget={entry.max_budget} budgetDecimals={2} />
|
||||
)}
|
||||
{rule && <span className="whitespace-nowrap text-xs tabular-nums text-muted-foreground">{rule}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemainingCell({ entry }: { entry: KeyBudgetEntry }) {
|
||||
const unlimited = entry.max_budget == null;
|
||||
return (
|
||||
|
|
@ -129,15 +153,7 @@ export const getKeyBudgetsTableColumns = (): ColumnDef<KeyBudgetEntry>[] => [
|
|||
header: "Spend / Limit",
|
||||
size: 200,
|
||||
enableSorting: false,
|
||||
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>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => <SpendCell entry={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "remaining",
|
||||
|
|
|
|||
|
|
@ -281,6 +281,31 @@ describe("KeyInfoView Budgets tab", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("does not render a spend the server could not read as a healthy $0.00", async () => {
|
||||
const unreadable: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
scope: "tag",
|
||||
entity_type: "tag",
|
||||
entity_id: "prod",
|
||||
entity_label: "prod",
|
||||
max_budget: 1000,
|
||||
spend: null,
|
||||
remaining: null,
|
||||
source: "budget_table:b-tag",
|
||||
status: "ok",
|
||||
note: "live spend could not be read",
|
||||
};
|
||||
mockBudgets([unreadable]);
|
||||
const panel = await renderAndOpenBudgetsTab();
|
||||
|
||||
const row = rowFor(panel, "prod");
|
||||
expect(row).toHaveTextContent("Unknown of $1,000.00");
|
||||
expect(row).not.toHaveTextContent("$0.00");
|
||||
// The meter is the part that lies loudest: drawn at 0% it reads as untouched headroom.
|
||||
expect(within(row).queryByRole("meter")).not.toBeInTheDocument();
|
||||
expect(row).toHaveTextContent("Blocks at ≥ $1,000.00");
|
||||
});
|
||||
|
||||
it("renders the longest note the endpoint can emit without dropping any of it", async () => {
|
||||
const endUser: KeyBudgetEntry = {
|
||||
...UNCONFIGURED_BUDGET,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue