fix(ui): send only a changed TPM limit from the team admin settings form
Some checks failed
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
LiteLLM Rust / rust-wheel (push) Has been cancelled

Save stays disabled until the value differs from the team's, so an unchanged form never reaches /team/update
This commit is contained in:
ryan-crabbe-berri 2026-09-16 16:37:35 -07:00
parent ab92a6637d
commit 3ee8d43fdd
4 changed files with 40 additions and 8 deletions

View file

@ -53,6 +53,18 @@ describe("TeamAdminSettingsForm", () => {
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ tpm_limit: null }));
});
it("keeps Save disabled until the TPM limit differs from the team's", () => {
renderForm(new Set(["tpm_limit"]));
const tpmInput = screen.getByLabelText("Tokens per minute Limit (TPM)");
const save = screen.getByRole("button", { name: /save changes/i });
expect(save).toBeDisabled();
fireEvent.change(tpmInput, { target: { value: "5000" } });
expect(save).toBeEnabled();
fireEvent.change(tpmInput, { target: { value: "1000" } });
expect(save).toBeDisabled();
});
it("closes without saving on cancel", async () => {
const user = userEvent.setup();
const { onSave, onCancel } = renderForm(new Set(["tpm_limit"]));
@ -65,6 +77,7 @@ describe("TeamAdminSettingsForm", () => {
it("locks both buttons while a save is in flight", () => {
renderForm(new Set(["tpm_limit"]), { isSaving: true });
fireEvent.change(screen.getByLabelText("Tokens per minute Limit (TPM)"), { target: { value: "5000" } });
expect(screen.getByRole("button", { name: "Cancel" })).toBeDisabled();
expect(screen.getByRole("button", { name: /save changes/i })).toBeDisabled();

View file

@ -1,6 +1,7 @@
"use client";
import { Save } from "lucide-react";
import { useWatch } from "react-hook-form";
import { z } from "zod/v4";
import { FormField } from "@/components/shared/form/FormField";
@ -37,7 +38,9 @@ export default function TeamAdminSettingsForm({
onSave,
}: TeamAdminSettingsFormProps) {
const form = useZodForm(teamAdminSettingsSchema, { defaultValues: initialValues });
const submit = form.handleSubmit((values) => onSave(teamAdminSettingsChanges(values, editableFields)));
const draft = useWatch({ control: form.control });
const hasChanges = Object.keys(teamAdminSettingsChanges(draft, initialValues, editableFields)).length > 0;
const submit = form.handleSubmit((values) => onSave(teamAdminSettingsChanges(values, initialValues, editableFields)));
return (
<form onSubmit={(event) => void submit(event)}>
@ -56,7 +59,7 @@ export default function TeamAdminSettingsForm({
<Button type="button" variant="outline" onClick={onCancel} disabled={isSaving}>
Cancel
</Button>
<Button type="submit" disabled={isSaving}>
<Button type="submit" disabled={isSaving || !hasChanges}>
{isSaving ? <UiLoadingSpinner className="size-4" /> : <Save className="size-4" />}
Save Changes
</Button>

View file

@ -20,21 +20,31 @@ describe("teamAdminFieldLabel", () => {
describe("teamAdminSettingsChanges", () => {
const tpmEnabled = new Set(["tpm_limit"]);
const stored = { tpm_limit: 1000 };
it.each([
["a typed number string", "5000", 5000],
["a stored number", 1200, 1200],
["a number", 1200, 1200],
["zero", "0", 0],
["an emptied input", "", null],
["whitespace", " ", null],
["no stored limit", null, null],
["no limit", null, null],
["an unset value", undefined, null],
])("sends tpm_limit for %s", (_label, tpm_limit, expected) => {
expect(teamAdminSettingsChanges({ tpm_limit }, tpmEnabled)).toStrictEqual({ tpm_limit: expected });
])("sends tpm_limit changed to %s", (_label, tpm_limit, expected) => {
expect(teamAdminSettingsChanges({ tpm_limit }, stored, tpmEnabled)).toStrictEqual({ tpm_limit: expected });
});
it.each([
["the stored number", 1000, { tpm_limit: 1000 }],
["the stored number typed back in", "1000", { tpm_limit: 1000 }],
["an emptied input over no stored limit", "", { tpm_limit: null }],
["an unset value over no stored limit", undefined, { tpm_limit: null }],
])("sends nothing for %s", (_label, tpm_limit, initialValues) => {
expect(teamAdminSettingsChanges({ tpm_limit }, initialValues, tpmEnabled)).toStrictEqual({});
});
it("leaves tpm_limit out when the proxy did not enable it for team admins", () => {
expect(teamAdminSettingsChanges({ tpm_limit: "5000" }, new Set(["max_budget"]))).toStrictEqual({});
expect(teamAdminSettingsChanges({ tpm_limit: "5000" }, stored, new Set(["max_budget"]))).toStrictEqual({});
});
});

View file

@ -59,8 +59,14 @@ const numberOrNull = (value: string | number | null | undefined): number | null
export const teamAdminSettingsChanges = (
values: TeamAdminSettingsValues,
initialValues: TeamAdminSettingsValues,
editableFields: ReadonlySet<string>,
): TeamAdminSettingsChanges => (editableFields.has("tpm_limit") ? { tpm_limit: numberOrNull(values.tpm_limit) } : {});
): TeamAdminSettingsChanges => {
const tpmLimit = numberOrNull(values.tpm_limit);
return editableFields.has("tpm_limit") && tpmLimit !== numberOrNull(initialValues.tpm_limit)
? { tpm_limit: tpmLimit }
: {};
};
export const parseTeamEditAccess = (callerEditAccess: unknown): TeamEditAccess => {
const parsed = callerEditAccessSchema.safeParse(callerEditAccess);