From 9dbf530d6e7719c13b2ab9e987cb994ce266b587 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 30 Jul 2026 18:44:45 -0700 Subject: [PATCH] fix(ui): let the internal user and org forms save sub-cent budgets The Default User Settings form on Internal Users, the org settings form and the org create dialog all rendered their money fields as `` inside a form that never opted out of native constraint validation. Any value with more than two decimals, such as a 0.001 max budget, failed the browser's step check, so Chrome vetoed the submit before react-hook-form ran. No request went out, no field error was shown, and the read view kept displaying the old value; it looked like the budget silently refused to stick. Money fields now use `step="any"`, and the three react-hook-form forms carry `noValidate` so zod stays the only validator and a DOM-level constraint can never swallow a submit again. --- .../DefaultUserSettingsForm.test.tsx | 28 +++++++++++++++++++ .../DefaultUserSettingsForm.tsx | 6 ++-- .../org-create/OrgCreateDialog.test.tsx | 22 +++++++++++++++ .../org-create/OrgCreateDialog.tsx | 4 +-- .../org-settings/OrgSettingsForm.test.tsx | 18 ++++++++++++ .../org-settings/OrgSettingsForm.tsx | 4 +-- 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx index bfdcc70fb0c..cc24931b2b3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.test.tsx @@ -149,6 +149,34 @@ describe("DefaultUserSettingsForm", () => { expect(updateSettings).toHaveBeenCalledWith({ ...SAVED_BODY, max_budget: 250 }); }); + it("saves a sub-cent budget the browser would veto under a 0.01 step", async () => { + const user = userEvent.setup(); + const { updateSettings } = renderForm(); + + await enterEditMode(user); + const budget: HTMLInputElement = await screen.findByLabelText("Max Budget (USD)"); + await user.clear(budget); + await user.type(budget, "0.001"); + + const teamBudget: HTMLInputElement = screen.getByLabelText("Max Budget in Team (USD)"); + await user.clear(teamBudget); + await user.type(teamBudget, "0.002"); + + // jsdom never blocks the submit itself, so assert the constraint the real browser + // enforces before handleSubmit ever runs + expect(budget.checkValidity()).toBe(true); + expect(teamBudget.checkValidity()).toBe(true); + + await user.click(await saveButton()); + + await waitFor(() => expect(updateSettings).toHaveBeenCalledTimes(1)); + expect(updateSettings).toHaveBeenCalledWith({ + ...SAVED_BODY, + max_budget: 0.001, + teams: [{ team_id: "team-alpha", max_budget_in_team: 0.002, user_role: "user" }], + }); + }); + it("clears an emptied budget with null", async () => { const user = userEvent.setup(); const { updateSettings } = renderForm(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx index b1474e7cd0c..1e0ec6b6998 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/users/_components/default-user-settings/DefaultUserSettingsForm.tsx @@ -133,7 +133,7 @@ const TeamsField = ({ control }: { control: SettingsControl }) => { {({ ref, ...budgetField }) => ( - + )} @@ -249,7 +249,7 @@ const SettingsForm = ({ initialValues, roleOptions, updateSettings, onCancel, on const onSubmit = form.handleSubmit((values) => mutation.mutate(values)); return ( -
+ - {({ ref, ...field }) => } + {({ ref, ...field }) => } { await waitFor(() => expect(screen.queryByLabelText("Organization Name")).not.toBeInTheDocument()); }); + it("creates with a sub-cent max budget the browser would veto under a 0.01 step", async () => { + const user = userEvent.setup(); + const { createOrganization } = renderDialog(); + + await user.type(screen.getByLabelText("Organization Name"), "new-org"); + const budget: HTMLInputElement = screen.getByLabelText("Max Budget (USD)"); + await user.type(budget, "0.001"); + + // jsdom never blocks the submit itself, so assert the constraint the real browser + // enforces before handleSubmit ever runs + expect(budget.checkValidity()).toBe(true); + + await user.click(screen.getByRole("button", { name: "Create Organization" })); + + await waitFor(() => expect(createOrganization).toHaveBeenCalledTimes(1)); + expect(createOrganization.mock.calls[0][0]).toStrictEqual({ + organization_alias: "new-org", + models: [], + max_budget: 0.001, + }); + }); + it("maps selectors and limits into the create body", async () => { const user = userEvent.setup(); const { createOrganization } = renderDialog(); diff --git a/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.tsx b/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.tsx index 998d9446365..4e1a00704e3 100644 --- a/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.tsx +++ b/ui/litellm-dashboard/src/components/organization/org-create/OrgCreateDialog.tsx @@ -79,7 +79,7 @@ export const OrgCreateDialog = ({ Create Organization - + {({ ref, ...field }) => } @@ -97,7 +97,7 @@ export const OrgCreateDialog = ({ - {({ ref, ...field }) => } + {({ ref, ...field }) => } diff --git a/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.test.tsx b/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.test.tsx index 5bd809bcfd5..4dfd37e3466 100644 --- a/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.test.tsx +++ b/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.test.tsx @@ -113,6 +113,24 @@ describe("OrgSettingsForm", () => { expect(patchOrganization).toHaveBeenCalledWith("org-1", { organization_alias: "acme-2" }); }); + it("saves a sub-cent max budget the browser would veto under a 0.01 step", async () => { + const user = userEvent.setup(); + const { patchOrganization } = renderForm(); + + const budget: HTMLInputElement = screen.getByLabelText("Max Budget (USD)"); + await user.clear(budget); + await user.type(budget, "0.001"); + + // jsdom never blocks the submit itself, so assert the constraint the real browser + // enforces before handleSubmit ever runs + expect(budget.checkValidity()).toBe(true); + + await user.click(screen.getByRole("button", { name: "Save Changes" })); + + await waitFor(() => expect(patchOrganization).toHaveBeenCalledTimes(1)); + expect(patchOrganization).toHaveBeenCalledWith("org-1", { max_budget: 0.001 }); + }); + it("sends null when a limit is cleared", async () => { const user = userEvent.setup(); const { patchOrganization } = renderForm(); diff --git a/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.tsx b/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.tsx index fe4965adb3c..affe0ed2d4e 100644 --- a/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.tsx +++ b/ui/litellm-dashboard/src/components/organization/org-settings/OrgSettingsForm.tsx @@ -78,7 +78,7 @@ export const OrgSettingsForm = ({ }); return ( - + {({ ref, ...field }) => } @@ -96,7 +96,7 @@ export const OrgSettingsForm = ({ - {({ ref, ...field }) => } + {({ ref, ...field }) => }