diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx index 1886075a9d9..40c82c51031 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.test.tsx @@ -600,6 +600,96 @@ describe("KeyEditView", () => { }); }); + it("should submit budget_limits: [] when the last budget window is deleted", async () => { + const onSubmitMock = vi.fn().mockResolvedValue(undefined); + const keyDataWithWindow = { + ...MOCK_KEY_DATA, + budget_limits: [{ budget_duration: "30d", max_budget: 100 }], + }; + renderWithProviders( + {}} + onSubmit={onSubmitMock} + accessToken={"test-token"} + userID={"test-user"} + userRole={"admin"} + premiumUser={false} + />, + ); + + const deleteWindowButton = await screen.findByRole("button", { name: "✕" }); + await userEvent.click(deleteWindowButton); + + const submitButton = screen.getByRole("button", { name: /save changes/i }); + await userEvent.click(submitButton); + + await waitFor(() => { + expect(onSubmitMock).toHaveBeenCalled(); + const callArgs = onSubmitMock.mock.calls[0][0]; + expect(callArgs.budget_limits).toEqual([]); + }); + }); + + it("should resend existing budget windows on submit when they are left untouched", async () => { + const onSubmitMock = vi.fn().mockResolvedValue(undefined); + const keyDataWithWindow = { + ...MOCK_KEY_DATA, + budget_limits: [{ budget_duration: "30d", max_budget: 100 }], + }; + renderWithProviders( + {}} + onSubmit={onSubmitMock} + accessToken={"test-token"} + userID={"test-user"} + userRole={"admin"} + premiumUser={false} + />, + ); + + const submitButton = await screen.findByRole("button", { name: /save changes/i }); + await userEvent.click(submitButton); + + await waitFor(() => { + expect(onSubmitMock).toHaveBeenCalled(); + const callArgs = onSubmitMock.mock.calls[0][0]; + expect(callArgs.budget_limits).toEqual([{ budget_duration: "30d", max_budget: 100 }]); + }); + }); + + it("should omit budget_limits (not clear stored windows) when a window is left incomplete", async () => { + const onSubmitMock = vi.fn().mockResolvedValue(undefined); + const keyDataWithWindow = { + ...MOCK_KEY_DATA, + budget_limits: [{ budget_duration: "30d", max_budget: 100 }], + }; + renderWithProviders( + {}} + onSubmit={onSubmitMock} + accessToken={"test-token"} + userID={"test-user"} + userRole={"admin"} + premiumUser={false} + />, + ); + + const maxBudgetInput = await screen.findByPlaceholderText("Max spend ($)"); + await userEvent.clear(maxBudgetInput); + + const submitButton = screen.getByRole("button", { name: /save changes/i }); + await userEvent.click(submitButton); + + await waitFor(() => { + expect(onSubmitMock).toHaveBeenCalled(); + const callArgs = onSubmitMock.mock.calls[0][0]; + expect(callArgs.budget_limits).toBeUndefined(); + }); + }); + it("should display 'AI APIs' label for the llm_api key type option", async () => { const keyDataWithLlmApiRoutes = { ...MOCK_KEY_DATA, diff --git a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 2f2d0097455..e991db8069e 100644 --- a/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -290,11 +290,19 @@ export function KeyEditView({ values.duration = null; } - // Include multi-window budget limits (filter out incomplete entries) + // Reconcile multi-window budget limits from the editor state, dropping + // incomplete entries (no max_budget). Sending [] tells the backend to clear + // all stored windows, so only send it when the user removed every window; + // when entries remain but are still incomplete, omit the field so the saved + // windows are left untouched (JSON.stringify drops the undefined key). const validWindows = budgetLimits.filter( (w) => w.budget_duration && w.max_budget !== null && w.max_budget !== undefined, ); - values.budget_limits = validWindows.length > 0 ? validWindows : undefined; + if (validWindows.length > 0) { + values.budget_limits = validWindows; + } else if (budgetLimits.length === 0) { + values.budget_limits = []; + } await onSubmit(values); } finally {