mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(ui): persist budget window deletion on virtual keys (#31107)
Deleting every budget window from a virtual key looked like it saved but reverted on reload, while editing a window persisted. The key edit form set budget_limits to undefined once the window list was emptied, and JSON.stringify drops undefined keys, so /key/update received no budget_limits field at all and model_dump(exclude_unset=True) skipped the existing clear-on-empty branch. Sending [] instead lets the backend store JSON null and clear the stored windows, matching how it already treats an explicit empty list Resolves LIT-3742
This commit is contained in:
parent
8bca05d311
commit
8f4389246d
2 changed files with 100 additions and 2 deletions
|
|
@ -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(
|
||||
<KeyEditView
|
||||
keyData={keyDataWithWindow}
|
||||
onCancel={() => {}}
|
||||
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(
|
||||
<KeyEditView
|
||||
keyData={keyDataWithWindow}
|
||||
onCancel={() => {}}
|
||||
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(
|
||||
<KeyEditView
|
||||
keyData={keyDataWithWindow}
|
||||
onCancel={() => {}}
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue