From 807dde81d634a411f589216769fe122d4a2bb248 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Wed, 1 Jul 2026 22:17:06 -0700 Subject: [PATCH] fix(ui): gate guardrails and credential name on isFieldTouched too Greptile flagged that guardrails and litellm_credential_name were added to the PATCH on every save regardless of whether the user changed them (an empty guardrails array is truthy, and the credential name was sent whenever non-empty), which both contradicts the send-only-what-changed design and lets an unrelated save re-assert a credential name that was changed elsewhere since the form was opened. Both are now gated on isFieldTouched like the other scalars; a touched-but-empty guardrails still sends [] to clear, and the credential name typed into the LiteLLM Params JSON is still stripped so the selector stays the source of truth --- .../src/components/model_info_view.test.tsx | 7 +++++-- ui/litellm-dashboard/src/components/model_info_view.tsx | 6 ++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ui/litellm-dashboard/src/components/model_info_view.test.tsx b/ui/litellm-dashboard/src/components/model_info_view.test.tsx index f22c5a7428a..feca098f5ad 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -544,7 +544,7 @@ describe("ModelInfoView", () => { }); }); - it("should keep selector credential and ignore litellm_credential_name from LiteLLM Params json", async () => { + it("ignores litellm_credential_name typed into the LiteLLM Params json and never re-sends the untouched selector credential", async () => { const user = userEvent.setup(); render(, { wrapper }); @@ -575,8 +575,9 @@ describe("ModelInfoView", () => { }); const updatePayload = mockModelPatchUpdateCall.mock.calls[0][1]; - expect(updatePayload.litellm_params.litellm_credential_name).toBe("selected-credential"); + expect(updatePayload.litellm_params.timeout).toBe(42); expect(updatePayload.litellm_params.litellm_credential_name).not.toBe("from-json"); + expect(updatePayload.litellm_params).not.toHaveProperty("litellm_credential_name"); }); it("should not include vector_store_ids in update payload when model has none", async () => { @@ -790,6 +791,8 @@ describe("ModelInfoView", () => { expect(updatePayload.litellm_params).not.toHaveProperty("organization"); expect(updatePayload.litellm_params).not.toHaveProperty("custom_llm_provider"); expect(updatePayload.litellm_params).not.toHaveProperty("stream_timeout"); + expect(updatePayload.litellm_params).not.toHaveProperty("guardrails"); + expect(updatePayload.litellm_params).not.toHaveProperty("litellm_credential_name"); }); it("never seeds or re-sends a masked secret nested inside an object (regression: nested-secret corruption)", async () => { diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index d5490f282e8..0b897938989 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -471,12 +471,10 @@ export default function ModelInfoView({ } } - if (values.litellm_credential_name) { + if (form.isFieldTouched("litellm_credential_name") && values.litellm_credential_name) { updatedLitellmParams.litellm_credential_name = values.litellm_credential_name; - } else { - delete updatedLitellmParams.litellm_credential_name; } - if (values.guardrails) { + if (form.isFieldTouched("guardrails")) { updatedLitellmParams.guardrails = values.guardrails; } if (values.vector_store_ids?.length > 0) {