From 6f390fc12e066c3b39a4f331f76f87dddd14b808 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 19 Aug 2026 18:34:35 -0700 Subject: [PATCH] refactor(ui): read throttling off enforcement instead of a caveat code `enforcement` now carries "throttled" as its own mode, so the table reads the field rather than inferring it from a note that only explains the mechanism. A row is throttling even when the note is absent, and a note without the mode no longer makes the table claim one. The enforcement badge is a lookup exhaustive over the union, so a fourth mode fails this build instead of silently defaulting to "Blocks requests", which is the claim that was wrong in the first place. --- .../(dashboard)/hooks/keys/useKeyBudgets.ts | 1 + .../templates/KeyBudgetsTableColumns.test.ts | 12 ++++- .../templates/KeyBudgetsTableColumns.tsx | 49 ++++++++++--------- .../key_info_view.budgets_tab.test.tsx | 3 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 10 ++-- 5 files changed, 45 insertions(+), 30 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeyBudgets.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeyBudgets.ts index c5c8fdd472b..b0069fe7c20 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeyBudgets.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/keys/useKeyBudgets.ts @@ -6,6 +6,7 @@ export type KeyBudgetsResponse = components["schemas"]["KeyBudgetsResponse"]; export type KeyBudgetEntry = KeyBudgetsResponse["budgets"][number]; export type KeyBudgetNote = KeyBudgetEntry["notes"][number]; export type KeyBudgetNoteCode = KeyBudgetNote["code"]; +export type KeyBudgetEnforcement = KeyBudgetEntry["enforcement"]; export const useKeyBudgets = (keyId: string | undefined) => { const { accessToken } = useAuthorized(); diff --git a/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.test.ts b/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.test.ts index 2fb92a43d8a..23f4268e344 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.test.ts +++ b/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.test.ts @@ -43,9 +43,10 @@ const ROLLING_NOTE = { text: noteText("rolling_window"), } as const; +// `enforcement` carries the fact now, so the note only explains the mechanism and drops to info. const THROTTLE_NOTE = { code: "throttled_instead_of_blocked", - severity: "warning", + severity: "info", text: noteText("throttled_instead_of_blocked"), } as const; @@ -220,7 +221,7 @@ describe("isThrottled", () => { it("never blames a throttled budget for a denial, because going over slows rather than rejects", () => { const throttledOver: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, - enforcement: "hard", + enforcement: "throttled", max_budget: 100, spend: 140, remaining: -40, @@ -232,6 +233,13 @@ describe("isThrottled", () => { expect(budgetThresholdRule(throttledOver)).toBe("Throttles at ≥ $100.00"); }); + it("reads the mode off enforcement rather than the note, which only explains the mechanism", () => { + const noNote: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, enforcement: "throttled", max_budget: 100, notes: [] }; + expect(isThrottled(noNote)).toBe(true); + const noteOnly: KeyBudgetEntry = { ...UNCONFIGURED_BUDGET, enforcement: "hard", notes: [THROTTLE_NOTE] }; + expect(isThrottled(noteOnly)).toBe(false); + }); + it("still says a plain hard budget blocks", () => { expect(isThrottled(TEAM_MEMBER_AT_LIMIT)).toBe(false); expect(budgetThresholdRule(TEAM_MEMBER_AT_LIMIT)).toBe("Blocks at ≥ $50.00"); diff --git a/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.tsx b/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.tsx index ceb2bf31bf2..f42dd75a571 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyBudgetsTableColumns.tsx @@ -2,7 +2,12 @@ import type { ColumnDef } from "@tanstack/react-table"; -import type { KeyBudgetEntry, KeyBudgetNote, KeyBudgetNoteCode } from "@/app/(dashboard)/hooks/keys/useKeyBudgets"; +import type { + KeyBudgetEnforcement, + KeyBudgetEntry, + KeyBudgetNote, + KeyBudgetNoteCode, +} from "@/app/(dashboard)/hooks/keys/useKeyBudgets"; import { CellTooltip, DateCell, @@ -65,8 +70,7 @@ const noteKillsRow = (note: KeyBudgetNote): boolean => { export const cannotTrip = (entry: KeyBudgetEntry): boolean => entry.notes.some(noteKillsRow); /** Exceeding a throttled budget slows requests rather than rejecting them, so it never denies one. */ -export const isThrottled = (entry: KeyBudgetEntry): boolean => - entry.notes.some((note) => note.code === "throttled_instead_of_blocked"); +export const isThrottled = (entry: KeyBudgetEntry): boolean => entry.enforcement === "throttled"; /** Whether going over this budget rejects a request, as opposed to alerting, throttling or nothing. */ const canDeny = (entry: KeyBudgetEntry): boolean => !isAlertOnly(entry) && !cannotTrip(entry) && !isThrottled(entry); @@ -159,28 +163,25 @@ function ScopeCell({ entry }: { entry: KeyBudgetEntry }) { ); } +/** Exhaustive, so a fourth enforcement mode fails this build rather than defaulting to a claim. */ +const ENFORCEMENT_BADGE: Readonly> = + { + soft: { + tone: "neutral", + label: "Alert only", + tooltip: "Soft budget. Going over raises an alert and never rejects a request.", + }, + throttled: { + tone: "warning", + label: "Throttles requests", + tooltip: "This key opted into throttle_on_budget_exceeded, so going over reduces its rate limits.", + }, + hard: { tone: "info", label: "Blocks requests", tooltip: "Going over this budget rejects requests on this key." }, + }; + function EnforcementCell({ entry }: { entry: KeyBudgetEntry }) { - if (isAlertOnly(entry)) { - return ( - - ); - } - if (isThrottled(entry)) { - return ( - - ); - } - return ( - - ); + const { tone, label, tooltip } = ENFORCEMENT_BADGE[entry.enforcement]; + return ; } function SpendCell({ entry }: { entry: KeyBudgetEntry }) { diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.budgets_tab.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.budgets_tab.test.tsx index b84e38f9064..9f910941d60 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.budgets_tab.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.budgets_tab.test.tsx @@ -574,10 +574,11 @@ describe("KeyInfoView Budgets tab", () => { remaining: -40, source: "key.max_budget", status: "exceeded", + enforcement: "throttled", notes: [ { code: "throttled_instead_of_blocked", - severity: "warning", + severity: "info", text: noteText("throttled_instead_of_blocked"), }, ], diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 6b5efd3b0ca..59d5476c868 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -6834,7 +6834,9 @@ export interface paths { * - entity_type: Litellm_EntityType - The entity a `BudgetExceededError` from this scope * names, so a denial message maps back to a row here * - entity_id / entity_label: str | None - Which entity is limited, and its human-facing alias - * - enforcement: str - `hard` blocks the request, `soft` only raises an alert + * - enforcement: str - `hard` blocks the request, `soft` only raises an alert, `throttled` + * scales the key's rate limits down instead of denying anything. Only the key's own + * `max_budget` can be `throttled`; every other scope on the same key still blocks * - max_budget: float | None - The limit in effect. `null` means this scope applies to the key * but places no limit on it * - spend: float | None - Spend as the enforcing check reads it, from the same cross-pod @@ -7532,7 +7534,9 @@ export interface paths { * - entity_type: Litellm_EntityType - The entity a `BudgetExceededError` from this scope * names, so a denial message maps back to a row here * - entity_id / entity_label: str | None - Which entity is limited, and its human-facing alias - * - enforcement: str - `hard` blocks the request, `soft` only raises an alert + * - enforcement: str - `hard` blocks the request, `soft` only raises an alert, `throttled` + * scales the key's rate limits down instead of denying anything. Only the key's own + * `max_budget` can be `throttled`; every other scope on the same key still blocks * - max_budget: float | None - The limit in effect. `null` means this scope applies to the key * but places no limit on it * - spend: float | None - Spend as the enforcing check reads it, from the same cross-pod @@ -26361,7 +26365,7 @@ export interface components { * Enforcement * @enum {string} */ - enforcement: "hard" | "soft"; + enforcement: "hard" | "soft" | "throttled"; /** Entity Id */ entity_id?: string | null; /** Entity Label */