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.
This commit is contained in:
ryan-crabbe-berri 2026-07-02 12:08:45 -07:00
parent 807dde81d6
commit 0438b26a24
2 changed files with 41 additions and 7 deletions

View file

@ -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(<ModelInfoView {...DEFAULT_ADMIN_PROPS} />, { 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,

View file

@ -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