diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerHierarchySettings.test.tsx b/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerHierarchySettings.integration.test.tsx similarity index 89% rename from litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerHierarchySettings.test.tsx rename to litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerHierarchySettings.integration.test.tsx index 16b7cf1760c..e5637c75bde 100644 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerHierarchySettings.test.tsx +++ b/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/CustomerHierarchySettings.integration.test.tsx @@ -35,7 +35,7 @@ const member: Member = { customer_member_id: "member-1", parent_team_id: "team-1", alias: "Member One", - budget_template_id: "template-1", + budget_id: "template-1", budget_override: { max_budget: 20 }, effective_budget: { max_budget: 20 }, budget_override_expires_at: "2026-12-01T12:00:00Z", @@ -116,13 +116,31 @@ describe("CustomerHierarchySettings", () => { expect(template).toHaveTextContent("Pro"); await user.click(screen.getByRole("button", { name: "Save changes" })); await waitFor(() => expect(onSaved).toHaveBeenCalledOnce()); - expect(await lastRequest().clone().json()).toEqual({ budget_template_id: "template-2" }); + expect(new URL(lastRequest().url).pathname).toBe("/management/v1/customer_members/member-1"); + expect(await lastRequest().clone().json()).toEqual({ budget_id: "template-2" }); + }); + + it("should clear the template without changing the existing override", async () => { + const user = userEvent.setup(); + fetchMock.mockResolvedValue(json({ ...member, budget_id: null })); + render( + , + ); + await chooseSelectOption(user, screen.getByRole("combobox", { name: "Budget template" }), "No template"); + await user.click(screen.getByRole("button", { name: "Save changes" })); + await waitFor(() => expect(onSaved).toHaveBeenCalledOnce()); + expect(await lastRequest().clone().json()).toEqual({ budget_id: null }); }); it("should identify an unavailable stored template instead of displaying No template", () => { render( { + it("should load the member template from the API response", () => { + expect(customerHierarchyFields({ kind: "member", record: member }).templateId).toBe(member.budget_id); + }); + + it("should allow an alias-only edit for a member using a template without an override", () => { + const entity: CustomerHierarchyEntity = { + kind: "member", + record: { ...member, budget_override: null, budget_override_expires_at: null }, + }; + expect(buildCustomerHierarchyPatch(entity, { ...customerHierarchyFields(entity), alias: "Updated" })).toEqual({ + ok: true, + patch: { kind: "member", body: { alias: "Updated" } }, + }); + }); + + it.each([ + ["template-2", "template-2"], + ["", null], + ])("should submit template selection %s without changing the override", (templateId, budgetId) => { + const entity: CustomerHierarchyEntity = { kind: "member", record: member }; + expect(buildCustomerHierarchyPatch(entity, { ...customerHierarchyFields(entity), templateId })).toEqual({ + ok: true, + patch: { kind: "member", body: { budget_id: budgetId } }, + }); + }); + it("should leave untouched settings out of the patch", () => { const entity: CustomerHierarchyEntity = { kind: "organization", record: organization }; expect(buildCustomerHierarchyPatch(entity, customerHierarchyFields(entity))).toEqual({ diff --git a/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerHierarchyForms.ts b/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerHierarchyForms.ts index cce0b915eb7..860ebfb4ab8 100644 --- a/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerHierarchyForms.ts +++ b/litellm/ui/litellm-dashboard/src/app/(dashboard)/customers/_components/customerHierarchyForms.ts @@ -66,7 +66,7 @@ export const customerHierarchyFields = (entity: CustomerHierarchyEntity): Custom tpm: asText(budget?.tpm_limit), rpm: asText(budget?.rpm_limit), allowedModels: budget?.allowed_models?.join("\n") ?? "", - templateId: entity.kind === "member" ? entity.record.budget_template_id ?? "" : "", + templateId: entity.kind === "member" ? entity.record.budget_id ?? "" : "", overrideExpiry: entity.kind === "member" ? localDateTime(entity.record.budget_override_expires_at) : "", }; }; @@ -125,7 +125,7 @@ const memberPatch = ( const initial = customerHierarchyFields({ kind: "member", record: member }); if (!fields.templateId && !fields.budgetEnabled) return { ok: false, error: "budgetRequired" }; const body: components["schemas"]["MemberPatch"] = aliasChanges(member.alias, fields.alias); - if (fields.templateId !== initial.templateId) body.budget_template_id = fields.templateId || null; + if (fields.templateId !== initial.templateId) body.budget_id = fields.templateId || null; if (changed) body.budget_override = budget; if (!fields.budgetEnabled && initial.budgetEnabled) body.budget_override_expires_at = null; if (fields.budgetEnabled && fields.overrideExpiry !== initial.overrideExpiry) {