From b5e757ce3af2f5bf05f9a28b9b06fb8ea4891a12 Mon Sep 17 00:00:00 2001 From: derhornspieler <15236687+derhornspieler@users.noreply.github.com> Date: Tue, 25 Aug 2026 10:43:46 -0400 Subject: [PATCH] fix(ui): treat a cleared credential field as a deletion A cleared field stays mounted carrying an empty value, so it was neither listed for deletion here nor sent in the update, since the caller drops empty values from the payload. The old value survived: clearing a destination left the previous one still configured and still receiving requests, which for a federated credential now carry a minted token. Emptiness counts as a deletion now. A masked but untouched value is a non-empty string, so it is still preserved. --- .../model_add/credential_form_helpers.test.ts | 7 +++++++ .../model_add/credential_form_helpers.ts | 14 ++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.test.ts b/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.test.ts index 759e4a43ccb..00109d925d0 100644 --- a/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.test.ts +++ b/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.test.ts @@ -98,6 +98,13 @@ describe("computeCredentialValuesToDelete", () => { expect(computeCredentialValuesToDelete(original, mounted)).toEqual([]); }); + it("deletes a field the operator cleared, since the caller drops it from the payload", () => { + const original = { api_base: "https://old.gateway.internal" }; + const mounted = { api_base: "" }; + + expect(computeCredentialValuesToDelete(original, mounted)).toEqual(["api_base"]); + }); + it("keeps a field the operator genuinely changed", () => { const original = { api_key: "sk-***1234" }; const mounted = { api_key: "sk-new-real-key" }; diff --git a/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.ts b/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.ts index 85d0d913db6..dc7245df1f1 100644 --- a/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.ts +++ b/ui/litellm-dashboard/src/components/model_add/credential_form_helpers.ts @@ -47,11 +47,21 @@ export function resetCredentialFormOnProviderChange( * * `mountedValues` must be the full projected form state (masked-but-untouched fields included), * not the caller's post-filter payload: a masked value that the operator never touched is still - * mounted and must be preserved, not read as "absent, so delete it". + * mounted and must be preserved, not read as "absent, so delete it". A masked value is a + * non-empty string, which is what separates it from a field the operator emptied. + * + * A cleared field stays mounted carrying an empty value, so emptiness counts as a deletion too. + * Without that it is neither deleted here nor sent in the update (the caller drops empty values + * from the payload), and the old value survives: clearing a destination would leave the previous + * one still receiving requests that now carry a minted federation token. */ export function computeCredentialValuesToDelete( originalValues: Record, mountedValues: Record, ): string[] { - return Object.keys(originalValues).filter((key) => !(key in mountedValues)); + return Object.keys(originalValues).filter((key) => { + if (!(key in mountedValues)) return true; + const value = mountedValues[key]; + return value === "" || value === null || value === undefined; + }); }