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.
This commit is contained in:
ryan-crabbe-berri 2026-08-19 18:34:35 -07:00
parent db6eec50ff
commit 6f390fc12e
5 changed files with 45 additions and 30 deletions

View file

@ -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();

View file

@ -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");

View file

@ -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<Record<KeyBudgetEnforcement, { tone: StatusTone; label: string; tooltip: string }>> =
{
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 (
<StatusBadge
tone="neutral"
label="Alert only"
tooltip="Soft budget. Going over raises an alert and never rejects a request."
/>
);
}
if (isThrottled(entry)) {
return (
<StatusBadge
tone="warning"
label="Throttles requests"
tooltip="This key opted into throttle_on_budget_exceeded, so going over slows requests instead of rejecting them."
/>
);
}
return (
<StatusBadge tone="info" label="Blocks requests" tooltip="Going over this budget rejects requests on this key." />
);
const { tone, label, tooltip } = ENFORCEMENT_BADGE[entry.enforcement];
return <StatusBadge tone={tone} label={label} tooltip={tooltip} />;
}
function SpendCell({ entry }: { entry: KeyBudgetEntry }) {

View file

@ -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"),
},
],

View file

@ -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 */