mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-26 01:12:21 +00:00
fix(proxy): drop cost-map metadata echoed back on model save
Filter unchanged cost-map fields from model-info save echoes while preserving edited overrides. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
5fc510a6fd
commit
52e1732fc9
3 changed files with 157 additions and 2 deletions
|
|
@ -137,7 +137,12 @@ from litellm.types.router import (
|
|||
updateDeployment,
|
||||
updateLiteLLMParams,
|
||||
)
|
||||
from litellm.types.utils import echoed_cost_map_pricing_fields, without_server_derived_pricing
|
||||
from litellm.types.utils import (
|
||||
COST_MAP_LOOKUP_KEY,
|
||||
echoed_cost_map_fields,
|
||||
echoed_cost_map_pricing_fields,
|
||||
without_server_derived_pricing,
|
||||
)
|
||||
from litellm.utils import get_utc_datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -871,6 +876,16 @@ def _ptu_priced_deployment(model_params: Deployment) -> Deployment:
|
|||
)
|
||||
|
||||
|
||||
def _cost_map_entry(model_info: Mapping[str, object]) -> Mapping[str, object]:
|
||||
key: Final = model_info.get(COST_MAP_LOOKUP_KEY)
|
||||
if not isinstance(key, str):
|
||||
return MappingProxyType({})
|
||||
try:
|
||||
return MappingProxyType(dict(litellm.get_model_info(model=key)))
|
||||
except Exception:
|
||||
return MappingProxyType({})
|
||||
|
||||
|
||||
def update_db_model(db_model: Deployment, updated_patch: updateDeployment) -> PrismaCompatibleUpdateDBModel:
|
||||
if updated_patch.model_info is not None:
|
||||
_raise_if_ptu_cost_attribution_disabled(updated_patch.model_info.model_dump(exclude_none=True))
|
||||
|
|
@ -893,7 +908,17 @@ def update_db_model(db_model: Deployment, updated_patch: updateDeployment) -> Pr
|
|||
|
||||
# update model info
|
||||
if updated_patch.model_info:
|
||||
merged_model_info.update(without_server_derived_pricing(updated_patch.model_info.model_dump(exclude_none=True)))
|
||||
incoming_model_info: Final = updated_patch.model_info.model_dump(exclude_none=True)
|
||||
echoed_fields: Final = echoed_cost_map_fields(incoming_model_info, _cost_map_entry(incoming_model_info))
|
||||
merged_model_info.update(
|
||||
MappingProxyType(
|
||||
dict(
|
||||
(k, v)
|
||||
for k, v in without_server_derived_pricing(incoming_model_info).items()
|
||||
if k not in echoed_fields
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
# Honor explicit-null clears LAST, after both merges, so a model_info blob a client
|
||||
# passes through cannot silently undo a litellm_params clear via .update().
|
||||
|
|
|
|||
|
|
@ -3758,6 +3758,18 @@ def echoed_cost_map_pricing_fields(model_info: Mapping[str, Any]) -> tuple[str,
|
|||
return tuple(sorted(k for k in model_info if is_server_derived_pricing_key(k)))
|
||||
|
||||
|
||||
def echoed_cost_map_fields(model_info: Mapping[str, Any], cost_map_entry: Mapping[str, Any]) -> tuple[str, ...]:
|
||||
"""Fields a ``/model/info`` echo copied from the cost map unchanged.
|
||||
|
||||
Only ``litellm.get_model_info`` emits ``key``, so a blob carrying it is an echo of that
|
||||
response. Anything in it that still equals the resolved cost-map entry is a display value
|
||||
nobody typed; a value the operator edited differs and stays a real override.
|
||||
"""
|
||||
if COST_MAP_LOOKUP_KEY not in model_info:
|
||||
return ()
|
||||
return tuple(sorted(k for k, v in model_info.items() if k in cost_map_entry and cost_map_entry[k] == v))
|
||||
|
||||
|
||||
def pricing_override_fields(*sources: Mapping[str, Any]) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
sorted(
|
||||
|
|
|
|||
|
|
@ -3949,6 +3949,124 @@ class TestModelInfoServerDerivedPricingFilter:
|
|||
assert written["access_groups"] == ["prod"]
|
||||
|
||||
|
||||
class TestModelInfoCostMapEchoFilter:
|
||||
"""LIT-5534. ``/model/info`` fills a deployment's ``model_info`` from the cost map (context
|
||||
limits, mode, provider, supported params, capability flags), and the Admin UI edit form sends
|
||||
that whole blob back on any save. Only values that still equal the cost-map entry are the
|
||||
echo; a value the operator changed is a real override and stays."""
|
||||
|
||||
def test_echoed_cost_map_metadata_is_not_persisted(self):
|
||||
import litellm
|
||||
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import update_db_model
|
||||
from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo
|
||||
|
||||
entry = litellm.get_model_info("gpt-5.6")
|
||||
echo = {**entry, "id": "dep-echo-0", "db_model": True, "access_groups": ["prod"]}
|
||||
db_model = Deployment(
|
||||
model_name="gpt-5.6",
|
||||
litellm_params=LiteLLM_Params(model="openai/gpt-5.6"),
|
||||
model_info=ModelInfo(id="dep-echo-0"),
|
||||
)
|
||||
|
||||
result = update_db_model(
|
||||
db_model=db_model,
|
||||
updated_patch=updateDeployment(model_info=ModelInfo(**echo)),
|
||||
)
|
||||
|
||||
info = json.loads(result["model_info"])
|
||||
assert info["access_groups"] == ["prod"]
|
||||
assert set(info).isdisjoint(entry)
|
||||
assert "max_input_tokens" not in info and "mode" not in info and "supports_vision" not in info, (
|
||||
"cost-map metadata must not be persisted from an unchanged /model/info echo"
|
||||
)
|
||||
|
||||
def test_an_edited_value_survives_the_echo_filter(self):
|
||||
import litellm
|
||||
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import update_db_model
|
||||
from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo
|
||||
|
||||
entry = litellm.get_model_info("gpt-5.6")
|
||||
echo = {
|
||||
**entry,
|
||||
"id": "dep-echo-1",
|
||||
"db_model": True,
|
||||
"access_groups": ["prod"],
|
||||
"max_input_tokens": entry["max_input_tokens"] + 1,
|
||||
"mode": "completion" if entry["mode"] != "completion" else "chat",
|
||||
}
|
||||
db_model = Deployment(
|
||||
model_name="gpt-5.6",
|
||||
litellm_params=LiteLLM_Params(model="openai/gpt-5.6"),
|
||||
model_info=ModelInfo(id="dep-echo-1"),
|
||||
)
|
||||
|
||||
result = update_db_model(
|
||||
db_model=db_model,
|
||||
updated_patch=updateDeployment(model_info=ModelInfo(**echo)),
|
||||
)
|
||||
|
||||
info = json.loads(result["model_info"])
|
||||
assert info["max_input_tokens"] == echo["max_input_tokens"]
|
||||
assert info["mode"] == echo["mode"]
|
||||
assert "litellm_provider" not in info
|
||||
assert "supported_openai_params" not in info
|
||||
|
||||
def test_metadata_without_a_cost_map_key_is_persisted(self):
|
||||
import litellm
|
||||
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import update_db_model
|
||||
from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo
|
||||
from litellm.types.utils import echoed_cost_map_fields
|
||||
|
||||
entry = litellm.get_model_info("gpt-5.6")
|
||||
assert echoed_cost_map_fields({"max_input_tokens": entry["max_input_tokens"]}, entry) == ()
|
||||
db_model = Deployment(
|
||||
model_name="gpt-5.6",
|
||||
litellm_params=LiteLLM_Params(model="openai/gpt-5.6"),
|
||||
model_info=ModelInfo(id="dep-echo-2"),
|
||||
)
|
||||
|
||||
result = update_db_model(
|
||||
db_model=db_model,
|
||||
updated_patch=updateDeployment(
|
||||
model_info=ModelInfo(
|
||||
id="dep-echo-2",
|
||||
max_input_tokens=entry["max_input_tokens"],
|
||||
mode=entry["mode"],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
info = json.loads(result["model_info"])
|
||||
assert info["max_input_tokens"] == entry["max_input_tokens"]
|
||||
assert info["mode"] == entry["mode"]
|
||||
|
||||
def test_a_stored_mode_survives_an_echoed_save(self):
|
||||
import litellm
|
||||
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import update_db_model
|
||||
from litellm.types.router import Deployment, LiteLLM_Params, ModelInfo
|
||||
|
||||
entry = litellm.get_model_info("gpt-5.6")
|
||||
db_model = Deployment(
|
||||
model_name="gpt-5.6",
|
||||
litellm_params=LiteLLM_Params(model="openai/gpt-5.6"),
|
||||
model_info=ModelInfo(id="dep-echo-3", mode=entry["mode"]),
|
||||
)
|
||||
echo = {**entry, "id": "dep-echo-3", "db_model": True, "access_groups": ["prod"]}
|
||||
|
||||
result = update_db_model(
|
||||
db_model=db_model,
|
||||
updated_patch=updateDeployment(model_info=ModelInfo(**echo)),
|
||||
)
|
||||
|
||||
info = json.loads(result["model_info"])
|
||||
assert info["mode"] == entry["mode"]
|
||||
assert "max_input_tokens" not in info
|
||||
|
||||
|
||||
class TestUpdateDBModelClearCacheControlInjectionPoints:
|
||||
def test_explicit_null_removes_stored_injection_points(self):
|
||||
from litellm.proxy.management_endpoints.model_management_endpoints import (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue