fix(model-edit): allow clearing custom pricing on wildcard models (#28719)

* fix(model-edit): allow clearing custom input/output cost on wildcard deployments

A user-set pricing override on a `/model/*` wildcard deployment could not
be removed: clearing the Input/Output Cost fields in the UI succeeded
visually, but the next read still showed the old values because both
`litellm_params` and `model_info` (mirrored via `SPECIAL_MODEL_INFO_PARAMS`)
retained the original rates.

UI: when the pricing field is touched but left empty, send `null` instead
of dropping it from the payload so the backend sees the clear intent. The
cache-read-cost fallback now guards against `null` as well as `undefined`
so a cleared input cost cannot silently wipe the cache-read override.

Backend: `update_db_model` honors explicit-null clears, but ONLY for
`SPECIAL_MODEL_INFO_PARAMS` (the 4 pricing fields). Restricting the
null-clear path prevents a team-scoped caller from using this codepath to
null out privileged fields like `team_id` or access groups.

Tests cover both clear paths (`litellm_params` and `model_info`), the
SPECIAL_MODEL_INFO_PARAMS mirror, PATCH semantics for omitted fields, and
the security guard that non-pricing nulls don't reach the merged dict.

Resolves LIT-3250

* fix(model-edit): run null-clears after both merges, not interleaved

The previous version cleared `model_info` from inside the litellm_params
merge block, but the subsequent `model_info.update(...)` re-injected the
old pricing because the UI's PATCH carries the full model_info blob with
the stale values still in it. Move the explicit-null clear pass to after
both merges so a model_info passthrough cannot resurrect cleared fields.

Adds a regression test for the realistic UI submit shape (both blobs in
the patch, model_info still holding the old pricing).

* test(e2e): clear-custom-pricing flow with create/delete cleanup

Covers the dashboard model edit form's pricing-clear flow end-to-end:
seeds a deployment with custom input/output pricing, drives the UI to
clear both fields, asserts the outgoing PATCH sends explicit nulls,
and confirms via /v2/model/info that the override is gone from both
litellm_params and model_info.

The dashboard DB persists across this suite, so beforeEach creates a
uniquely-named deployment and afterEach POSTs /model/delete to leave
the DB clean regardless of test outcome.

* fix(model-edit): extend pricing clear to cache_read and cache_write costs

Pre-existing parallel of the wildcard input/output cost bug: cleared
cache_read_input_token_cost and cache_creation_input_token_cost overrides
silently persisted because the UI omitted the key (delete or fallback) and
the backend null-clear allowlist did not cover them.

- types/router.py: add cache_read_input_token_cost and
  cache_creation_input_token_cost to SPECIAL_MODEL_INFO_PARAMS, so they are
  mirrored between litellm_params and model_info by Deployment.__init__ and
  honoured by the null-clear loop in update_db_model.
- model_info_view.tsx: emit explicit null for touched-but-empty cache_read
  and cache_write fields. Preserve the input_cost->cache_read mirror only
  when cache_read itself was not touched.
- model_management_endpoints.py: update the allowlist comment.
- Tests: three new unit tests for cache clear paths and a preserve check;
  the e2e spec now seeds, clears, and asserts null PATCH + key-absence for
  all four pricing fields.
This commit is contained in:
ryan-crabbe-berri 2026-05-26 09:37:23 -07:00 committed by GitHub
parent a8263cbc88
commit f75a7c6b22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 532 additions and 10 deletions

View File

@ -51,6 +51,7 @@ from litellm.types.proxy.management_endpoints.model_management_endpoints import
UpdateUsefulLinksRequest,
)
from litellm.types.router import (
SPECIAL_MODEL_INFO_PARAMS,
Deployment,
DeploymentTypedDict,
LiteLLMParamsTypedDict,
@ -130,6 +131,32 @@ def update_db_model(
updated_patch.model_info.model_dump(exclude_none=True)
)
# Honor explicit-null clears LAST, after both merges, so a model_info blob the UI
# passes through (which today re-sends the OLD pricing on every save) cannot
# silently undo a litellm_params clear via .update().
#
# Restricted to SPECIAL_MODEL_INFO_PARAMS (input/output cost per token/character
# and cache read/write costs) so this path cannot be used to null out privileged
# model_info fields like team_id or access groups. SPECIAL_MODEL_INFO_PARAMS are
# mirrored between litellm_params and model_info by Deployment.__init__, so the
# clear propagates to both blobs.
if updated_patch.litellm_params:
for field in updated_patch.litellm_params.model_fields_set:
if (
field in SPECIAL_MODEL_INFO_PARAMS
and getattr(updated_patch.litellm_params, field) is None
):
merged_deployment_dict["litellm_params"].pop(field, None) # type: ignore
merged_deployment_dict.get("model_info", {}).pop(field, None)
if updated_patch.model_info:
for field in updated_patch.model_info.model_fields_set:
if (
field in SPECIAL_MODEL_INFO_PARAMS
and getattr(updated_patch.model_info, field) is None
):
merged_deployment_dict["model_info"].pop(field, None) # type: ignore
merged_deployment_dict.get("litellm_params", {}).pop(field, None) # type: ignore
# convert to prisma compatible format
prisma_compatible_model_dict = PrismaCompatibleUpdateDBModel()

View File

@ -398,6 +398,8 @@ SPECIAL_MODEL_INFO_PARAMS = [
"output_cost_per_token",
"input_cost_per_character",
"output_cost_per_character",
"cache_read_input_token_cost",
"cache_creation_input_token_cost",
]

View File

@ -1497,6 +1497,305 @@ class TestUpdateDBModelBlocked:
assert "blocked" not in result
def _build_db_model_with_pricing():
"""Wildcard deployment with custom pricing in litellm_params; Deployment.__init__
mirrors SPECIAL_MODEL_INFO_PARAMS into model_info, so both blobs hold the rate."""
from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo
return Deployment(
model_name="openai/*",
litellm_params=LiteLLM_Params(
model="openai/*",
input_cost_per_token=0.000001,
output_cost_per_token=0.000002,
),
model_info=ModelInfo(id="dep-pricing-0"),
)
class TestUpdateDBModelClearPricing:
"""Sending an explicit `null` for a pricing field must remove it from both
`litellm_params` and `model_info` (SPECIAL_MODEL_INFO_PARAMS are mirrored
between the two by Deployment.__init__).
Restricted to SPECIAL_MODEL_INFO_PARAMS so non-pricing fields (e.g. team_id)
cannot be cleared via this path.
"""
def test_clear_input_cost_removes_from_both_blobs(self):
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import updateLiteLLMParams
result = update_db_model(
db_model=_build_db_model_with_pricing(),
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(input_cost_per_token=None)
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "input_cost_per_token" not in params
assert "input_cost_per_token" not in info
# Other pricing untouched
assert params.get("output_cost_per_token") == 0.000002
assert info.get("output_cost_per_token") == 0.000002
def test_clear_output_cost_removes_from_both_blobs(self):
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import updateLiteLLMParams
result = update_db_model(
db_model=_build_db_model_with_pricing(),
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(output_cost_per_token=None)
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "output_cost_per_token" not in params
assert "output_cost_per_token" not in info
def test_non_null_pricing_update_still_works(self):
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import updateLiteLLMParams
result = update_db_model(
db_model=_build_db_model_with_pricing(),
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(input_cost_per_token=0.000005)
),
)
params = json.loads(result["litellm_params"])
assert params["input_cost_per_token"] == 0.000005
def test_omitted_pricing_field_is_preserved(self):
"""PATCH semantics: fields not in the patch keep their existing value."""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import updateLiteLLMParams
result = update_db_model(
db_model=_build_db_model_with_pricing(),
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(output_cost_per_token=0.000007)
),
)
params = json.loads(result["litellm_params"])
assert params["input_cost_per_token"] == 0.000001
assert params["output_cost_per_token"] == 0.000007
def test_null_on_non_pricing_field_does_not_clear(self):
"""Security guard: only SPECIAL_MODEL_INFO_PARAMS can be cleared via null.
Privileged or unrelated model_info fields (e.g. team_id) must be unaffected
by the null-clearing path so a team admin can't ungate a team-scoped model.
"""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import (
Deployment,
LiteLLM_Params,
ModelInfo,
updateLiteLLMParams,
)
db_model = Deployment(
model_name="openai/*",
litellm_params=LiteLLM_Params(
model="openai/*",
input_cost_per_token=0.000001,
),
model_info=ModelInfo(id="dep-pricing-1", team_id="team-keep-me"),
)
# Patch sends a null for api_base (non-SPECIAL field). Must NOT clear team_id
# or any other non-pricing field from the merged dict.
result = update_db_model(
db_model=db_model,
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(api_base=None)
),
)
info = json.loads(result["model_info"])
# Pricing still present (not part of this patch)
assert "input_cost_per_token" in info
# team_id must survive
assert info.get("team_id") == "team-keep-me"
def test_clear_survives_model_info_passthrough_with_old_pricing(self):
"""Realistic UI submit shape: the patch carries BOTH blobs. The
model_info portion still has the old pricing because the form
re-serializes the source blob. The litellm_params null must beat the
model_info merge i.e. the clear runs after both merges, not between.
"""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import ModelInfo, updateLiteLLMParams
result = update_db_model(
db_model=_build_db_model_with_pricing(),
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(input_cost_per_token=None),
# The UI passes the OLD model_info blob through unchanged.
model_info=ModelInfo(
id="dep-pricing-0",
input_cost_per_token=0.000001, # stale value from the page state
),
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "input_cost_per_token" not in params
assert (
"input_cost_per_token" not in info
), "model_info passthrough must not resurrect the cleared override"
def test_clear_via_model_info_clears_both_blobs(self):
"""The mirror works in the reverse direction too: nulling a pricing field
via the model_info patch should clear it from litellm_params as well."""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import ModelInfo
result = update_db_model(
db_model=_build_db_model_with_pricing(),
updated_patch=updateDeployment(
model_info=ModelInfo(id="dep-pricing-0", input_cost_per_token=None)
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "input_cost_per_token" not in params
assert "input_cost_per_token" not in info
def test_clear_cache_read_cost_removes_from_both_blobs(self):
"""cache_read_input_token_cost was added to SPECIAL_MODEL_INFO_PARAMS so
the same null-clear path works for cache-read overrides."""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import (
Deployment,
LiteLLM_Params,
ModelInfo,
updateLiteLLMParams,
)
db_model = Deployment(
model_name="openai/*",
litellm_params=LiteLLM_Params(
model="openai/*",
cache_read_input_token_cost=0.0000005,
),
model_info=ModelInfo(id="dep-cache-read-0"),
)
result = update_db_model(
db_model=db_model,
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(cache_read_input_token_cost=None)
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "cache_read_input_token_cost" not in params
assert "cache_read_input_token_cost" not in info
def test_clear_cache_write_cost_removes_from_both_blobs(self):
"""cache_creation_input_token_cost was added to SPECIAL_MODEL_INFO_PARAMS so
the same null-clear path works for cache-write overrides."""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import (
Deployment,
LiteLLM_Params,
ModelInfo,
updateLiteLLMParams,
)
db_model = Deployment(
model_name="openai/*",
litellm_params=LiteLLM_Params(
model="openai/*",
cache_creation_input_token_cost=0.000003,
),
model_info=ModelInfo(id="dep-cache-write-0"),
)
result = update_db_model(
db_model=db_model,
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(cache_creation_input_token_cost=None)
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "cache_creation_input_token_cost" not in params
assert "cache_creation_input_token_cost" not in info
def test_clear_cache_read_preserves_other_pricing(self):
"""Clearing cache_read must not touch input/output cost overrides."""
from litellm.proxy.management_endpoints.model_management_endpoints import (
update_db_model,
)
from litellm.types.router import (
Deployment,
LiteLLM_Params,
ModelInfo,
updateLiteLLMParams,
)
db_model = Deployment(
model_name="openai/*",
litellm_params=LiteLLM_Params(
model="openai/*",
input_cost_per_token=0.000001,
output_cost_per_token=0.000002,
cache_read_input_token_cost=0.0000005,
cache_creation_input_token_cost=0.000003,
),
model_info=ModelInfo(id="dep-cache-mixed-0"),
)
result = update_db_model(
db_model=db_model,
updated_patch=updateDeployment(
litellm_params=updateLiteLLMParams(cache_read_input_token_cost=None)
),
)
params = json.loads(result["litellm_params"])
info = json.loads(result["model_info"])
assert "cache_read_input_token_cost" not in params
assert "cache_read_input_token_cost" not in info
# Other pricing untouched in both blobs
assert params["input_cost_per_token"] == 0.000001
assert params["output_cost_per_token"] == 0.000002
assert params["cache_creation_input_token_cost"] == 0.000003
assert info["input_cost_per_token"] == 0.000001
assert info["output_cost_per_token"] == 0.000002
assert info["cache_creation_input_token_cost"] == 0.000003
class TestGetModelInfoWithIdBlocked:
"""`ProxyConfig.get_model_info_with_id` must propagate the DB-level `blocked`
column into the in-memory `model_info` dict so the router filter can read it."""

View File

@ -0,0 +1,177 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH } from "../../constants";
import { Role, users } from "../../fixtures/users";
/**
* Regression: clearing the Input / Output / Cache Read / Cache Write Cost
* fields on a deployment with a user-set pricing override must actually remove
* the override from both `litellm_params` and `model_info`.
*
* Pre-fix, the UI sent the old pricing back on every save (the spread of
* `values.litellm_params` re-injected it), and the backend's `exclude_none=True`
* stripped any null that did make it through. End-result: the dashboard
* displayed "Saved" but the override remained in the DB. The cache fields had
* the same bug in a parallel code path and are covered here too.
*/
test.describe("Clear custom pricing on a deployment", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
const masterKey = users[Role.ProxyAdmin].password;
const SEED_INPUT_PER_TOKEN = 0.0000777;
const SEED_OUTPUT_PER_TOKEN = 0.0000999;
const SEED_CACHE_READ_PER_TOKEN = 0.0000333;
const SEED_CACHE_WRITE_PER_TOKEN = 0.0000555;
// Unique-per-run name so concurrent / repeated runs don't collide on the
// shared dashboard DB. Captured here so afterEach can clean it up.
let createdModelId: string | null = null;
let modelName: string;
test.beforeEach(async ({ page }) => {
modelName = `e2e-clear-pricing-${Date.now()}`;
const res = await page.request.post("/model/new", {
headers: { Authorization: `Bearer ${masterKey}` },
data: {
model_name: modelName,
litellm_params: {
model: "openai/gpt-4o",
api_key: "sk-e2e-not-used",
input_cost_per_token: SEED_INPUT_PER_TOKEN,
output_cost_per_token: SEED_OUTPUT_PER_TOKEN,
cache_read_input_token_cost: SEED_CACHE_READ_PER_TOKEN,
cache_creation_input_token_cost: SEED_CACHE_WRITE_PER_TOKEN,
},
model_info: {},
},
});
expect(res.ok(), `POST /model/new for ${modelName}`).toBe(true);
const body = await res.json();
createdModelId = body.model_info?.id ?? body.model_id;
expect(createdModelId, "model id from /model/new").toBeTruthy();
});
test.afterEach(async ({ page }) => {
// The dashboard DB persists across this suite (not just per-test), so every
// model created here must be cleaned up regardless of test outcome.
if (createdModelId) {
await page.request.post("/model/delete", {
headers: { Authorization: `Bearer ${masterKey}` },
data: { id: createdModelId },
});
createdModelId = null;
}
});
test("UI sends null for cleared pricing and backend removes the override", async ({
page,
}) => {
// Navigate to the model detail view.
await page.goto("/ui");
await page.getByText("Models + Endpoints").click();
const modelRow = page.locator("tr", { hasText: modelName }).first();
await expect(modelRow).toBeVisible({ timeout: 15_000 });
await modelRow.click();
await expect(page.getByText("Back to Models").first()).toBeVisible({
timeout: 10_000,
});
// Sanity: the seeded pricing is shown in the detail view (77.7000 / 99.9000
// per 1M tokens). The dashboard renders the per-token rate × 1e6.
await expect(page.getByText("77.7000")).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("99.9000")).toBeVisible({ timeout: 10_000 });
// Open the edit form and clear all four pricing fields.
await page.getByRole("button", { name: "Edit Settings" }).click();
const inputCost = page.getByPlaceholder("Enter input cost");
const outputCost = page.getByPlaceholder("Enter output cost");
// Both cache fields share the same placeholder ("Defaults to Input Cost if blank"),
// so disambiguate via the Form.Item id (AntD assigns the `name` prop as input id).
const cacheReadCost = page.locator("#cache_read_cost");
const cacheWriteCost = page.locator("#cache_write_cost");
await inputCost.waitFor({ timeout: 15_000 });
for (const field of [inputCost, outputCost, cacheReadCost, cacheWriteCost]) {
await field.click({ clickCount: 3 });
await page.keyboard.press("Delete");
}
// Capture the outgoing PATCH so we can assert the UI sends explicit nulls.
const patchPromise = page.waitForRequest(
(req) =>
req.method() === "PATCH" &&
req.url().includes(`/model/${createdModelId}/update`)
);
await page.getByRole("button", { name: "Save Changes" }).click();
const patchReq = await patchPromise;
const patchBody = JSON.parse(patchReq.postData() ?? "{}");
expect(
patchBody.litellm_params.input_cost_per_token,
"UI sends explicit null for cleared input cost"
).toBeNull();
expect(
patchBody.litellm_params.output_cost_per_token,
"UI sends explicit null for cleared output cost"
).toBeNull();
expect(
patchBody.litellm_params.cache_read_input_token_cost,
"UI sends explicit null for cleared cache_read cost"
).toBeNull();
expect(
patchBody.litellm_params.cache_creation_input_token_cost,
"UI sends explicit null for cleared cache_write cost"
).toBeNull();
// Success toast confirms the save was accepted.
await expect(
page.getByText("Model settings updated successfully")
).toBeVisible({ timeout: 10_000 });
// Verify via the management API: the user-set rate is gone from both blobs.
// The cost-map may synthesize a default for known providers in the response,
// so the assertion is "no longer the seeded value" rather than literally
// undefined.
const infoRes = await page.request.get(
`/v2/model/info?include_team_models=true&page=1&size=100&modelId=${createdModelId}`,
{ headers: { Authorization: `Bearer ${masterKey}` } }
);
expect(infoRes.ok()).toBe(true);
const infoBody = await infoRes.json();
const row = (infoBody.data ?? infoBody).find?.(
(m: any) => m?.model_info?.id === createdModelId
);
expect(row, "model info row").toBeTruthy();
expect(
"input_cost_per_token" in row.litellm_params,
"litellm_params.input_cost_per_token key removed"
).toBe(false);
expect(
"output_cost_per_token" in row.litellm_params,
"litellm_params.output_cost_per_token key removed"
).toBe(false);
expect(
"cache_read_input_token_cost" in row.litellm_params,
"litellm_params.cache_read_input_token_cost key removed"
).toBe(false);
expect(
"cache_creation_input_token_cost" in row.litellm_params,
"litellm_params.cache_creation_input_token_cost key removed"
).toBe(false);
expect(
row.model_info.input_cost_per_token,
"model_info.input_cost_per_token no longer the seeded override"
).not.toBe(SEED_INPUT_PER_TOKEN);
expect(
row.model_info.output_cost_per_token,
"model_info.output_cost_per_token no longer the seeded override"
).not.toBe(SEED_OUTPUT_PER_TOKEN);
expect(
row.model_info.cache_read_input_token_cost,
"model_info.cache_read_input_token_cost no longer the seeded override"
).not.toBe(SEED_CACHE_READ_PER_TOKEN);
expect(
row.model_info.cache_creation_input_token_cost,
"model_info.cache_creation_input_token_cost no longer the seeded override"
).not.toBe(SEED_CACHE_WRITE_PER_TOKEN);
});
});

View File

@ -256,14 +256,26 @@ export default function ModelInfoView({
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("input_cost")) {
if (values.input_cost !== undefined && values.input_cost !== null && values.input_cost !== "") {
updatedLitellmParams.input_cost_per_token = Number(values.input_cost) / 1_000_000;
} else {
// Explicit null signals the backend to remove the pricing override.
updatedLitellmParams.input_cost_per_token = null;
}
}
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 (form.isFieldTouched("output_cost")) {
if (values.output_cost !== undefined && values.output_cost !== null && values.output_cost !== "") {
updatedLitellmParams.output_cost_per_token = Number(values.output_cost) / 1_000_000;
} else {
updatedLitellmParams.output_cost_per_token = null;
}
}
// Cache Read Cost: explicit value if provided, else fall back to input cost (when input cost touched).
// Cache Read Cost:
// - explicit value provided → use it
// - field touched but empty → explicit null (signals backend to remove override)
// - only input_cost touched → fall back to input_cost (guarded against null)
if (form.isFieldTouched("cache_read_cost") || form.isFieldTouched("input_cost")) {
if (
values.cache_read_cost !== undefined &&
@ -271,14 +283,19 @@ export default function ModelInfoView({
values.cache_read_cost !== ""
) {
updatedLitellmParams.cache_read_input_token_cost = Number(values.cache_read_cost) / 1_000_000;
} else if (updatedLitellmParams.input_cost_per_token !== undefined) {
} else if (form.isFieldTouched("cache_read_cost")) {
updatedLitellmParams.cache_read_input_token_cost = null;
} else if (
updatedLitellmParams.input_cost_per_token !== undefined &&
updatedLitellmParams.input_cost_per_token !== null
) {
updatedLitellmParams.cache_read_input_token_cost = updatedLitellmParams.input_cost_per_token;
}
}
// Cache Write Cost: explicit value if provided, else clear the override
// so the backend falls back to the model-level default. Sending 0 here
// would persist a zero rate even when the user intended to unset it.
// Cache Write Cost: explicit value if provided, else explicit null so the
// backend removes the override and falls back to the model-level default.
// Sending 0 here would persist a zero rate even when the user intended to unset it.
if (form.isFieldTouched("cache_write_cost")) {
if (
values.cache_write_cost !== undefined &&
@ -287,7 +304,7 @@ export default function ModelInfoView({
) {
updatedLitellmParams.cache_creation_input_token_cost = Number(values.cache_write_cost) / 1_000_000;
} else {
delete updatedLitellmParams.cache_creation_input_token_cost;
updatedLitellmParams.cache_creation_input_token_cost = null;
}
}