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; + }); }