fix(ui): stop injecting $0 cost into model update payload when cost fields are untouched

Editing a model in the Admin UI (e.g. to change credential) unconditionally
included input_cost_per_token: 0 and output_cost_per_token: 0 in the PATCH
payload, overriding built-in pricing from model_prices_and_context_window.json.

Guard cost fields with form.isFieldTouched so they are only sent when the
user explicitly modifies them. Intentional $0 cost (for budget bypass) still
works because the guard checks touched + non-null, not non-zero.
This commit is contained in:
Ryan Crabbe
2026-04-17 22:25:31 -07:00
parent 850fe595ac
commit c3d94d08cf
2 changed files with 36 additions and 2 deletions
@@ -579,6 +579,34 @@ describe("ModelInfoView", () => {
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,
// overriding the built-in pricing table from model_prices_and_context_window.json.
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 }));
await waitFor(() => {
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
});
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).not.toHaveProperty("input_cost_per_token");
expect(updatePayload.litellm_params).not.toHaveProperty("output_cost_per_token");
});
it("should display health check model field for wildcard models", async () => {
const wildcardModelData = {
...defaultModelData,
@@ -253,10 +253,16 @@ export default function ModelInfoView({
max_retries: values.max_retries,
timeout: values.timeout,
stream_timeout: values.stream_timeout,
input_cost_per_token: values.input_cost / 1_000_000,
output_cost_per_token: values.output_cost / 1_000_000,
tags: values.tags,
};
if (form.isFieldTouched("input_cost") && values.input_cost !== undefined && values.input_cost !== null) {
updatedLitellmParams.input_cost_per_token = Number(values.input_cost) / 1_000_000;
}
if (form.isFieldTouched("output_cost") && values.output_cost !== undefined && values.output_cost !== null) {
updatedLitellmParams.output_cost_per_token = Number(values.output_cost) / 1_000_000;
}
if (values.litellm_credential_name) {
updatedLitellmParams.litellm_credential_name = values.litellm_credential_name;
} else {