From 0438b26a24a0f82d47e07058506e84ab3aef0897 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Thu, 2 Jul 2026 12:08:45 -0700 Subject: [PATCH] fix(ui): gate vector_store_ids on isFieldTouched too Completing the send-only-what-changed audit: vector_store_ids was still value-conditional, so a model that has vector stores re-sent them on every save. It's now touch-gated like the other fields while preserving the tri-state clear (a touched-but-emptied selection sends [] to clear on the backend, an untouched one is omitted so the stored value is kept). cache_control_injection_points is deliberately left ungated: CacheControlSettings writes it through form.setFieldValue("litellm_extra_params", ...), which never marks a field touched, so its real edits already ride the textarea value-diff; an isFieldTouched gate there would silently drop cache-control changes. --- .../src/components/model_info_view.test.tsx | 39 +++++++++++++++++++ .../src/components/model_info_view.tsx | 9 +---- 2 files changed, 41 insertions(+), 7 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 feca098f5ad..f45033f66e5 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.test.tsx @@ -607,6 +607,45 @@ describe("ModelInfoView", () => { expect(updatePayload.litellm_params).not.toHaveProperty("vector_store_ids"); }); + it("does not re-send vector_store_ids when the user changes an unrelated field", async () => { + const modelWithVectorStores = { + ...defaultModelData, + litellm_params: { + model: "gpt-4", + api_base: "https://api.openai.com/v1", + custom_llm_provider: "openai", + vector_store_ids: ["vs_existing"], + }, + }; + mockUseModelsInfo.mockReturnValue({ + data: { data: [modelWithVectorStores] }, + isLoading: false, + error: null, + }); + mockModelInfoV1Call.mockResolvedValue({ data: [modelWithVectorStores] }); + + const user = userEvent.setup(); + render(, { wrapper }); + + await waitFor(() => { + expect(screen.getByRole("button", { name: /edit settings/i })).toBeInTheDocument(); + }); + await user.click(screen.getByRole("button", { name: /edit settings/i })); + + const tpmInput = await screen.findByPlaceholderText("Enter TPM"); + await user.type(tpmInput, "100"); + + await user.click(screen.getByRole("button", { name: /save changes/i })); + + await waitFor(() => { + expect(mockModelPatchUpdateCall).toHaveBeenCalled(); + }); + + const updatePayload = mockModelPatchUpdateCall.mock.calls[0][1]; + expect(updatePayload.litellm_params).toHaveProperty("tpm"); + expect(updatePayload.litellm_params).not.toHaveProperty("vector_store_ids"); + }); + it("should not include input_cost_per_token or output_cost_per_token in update payload when user does not touch cost fields", async () => { // Regression: editing a model without touching cost fields used to inject // input_cost_per_token: 0 and output_cost_per_token: 0 into litellm_params, diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 0b897938989..e92b9948bdc 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -477,13 +477,8 @@ export default function ModelInfoView({ if (form.isFieldTouched("guardrails")) { updatedLitellmParams.guardrails = values.guardrails; } - if (values.vector_store_ids?.length > 0) { - updatedLitellmParams.vector_store_ids = values.vector_store_ids; - } else if (values.vector_store_ids !== undefined) { - // User explicitly cleared previously-set vector stores — send [] to clear on backend - updatedLitellmParams.vector_store_ids = []; - } else { - delete updatedLitellmParams.vector_store_ids; + if (form.isFieldTouched("vector_store_ids")) { + updatedLitellmParams.vector_store_ids = values.vector_store_ids?.length > 0 ? values.vector_store_ids : []; } // Handle cache control settings