Merge pull request #34041 from BerriAI/litellm_lit4544_shared_key_model_info_leak

fix(router): stop custom model_info leaking onto shared backend cost map key
This commit is contained in:
Mateo Wang 2026-07-20 19:22:57 -04:00 • committed by GitHub
commit fa59cfa6da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 169 additions and 12 deletions

View file

@ -200,6 +200,7 @@ from litellm.types.utils import (
CustomPricingLiteLLMParams,
GenericBudgetConfigType,
LiteLLMBatch,
shared_backend_model_info,
)
from litellm.types.utils import ModelInfo
from litellm.types.utils import ModelInfo as ModelMapInfo
@ -7495,12 +7496,13 @@ class Router:
if deployment.litellm_params.custom_llm_provider is not None:
_model_name = deployment.litellm_params.custom_llm_provider + "/" + _model_name
# For the shared backend key, strip custom pricing fields so that
# one deployment's pricing overrides don't pollute another
# deployment sharing the same backend model name.
# Each deployment's full pricing is already stored under its
# unique model_id above.
_shared_model_info = CustomPricingLiteLLMParams.strip_custom_pricing_fields(_model_info)
# For the shared backend key, keep only cost-map schema fields
# (minus custom pricing) so that one deployment's pricing overrides
# or custom metadata (id, access_via_team_ids, arbitrary keys)
# don't pollute another deployment sharing the same backend model
# name. Each deployment's full model_info is already stored under
# its unique model_id above.
_shared_model_info = shared_backend_model_info(_model_info)
_existing_shared_mode = (cast(Optional[dict], litellm.model_cost.get(_model_name, {})) or {}).get("mode")
_deployment_mode = _shared_model_info.get("mode")
# Keep the built-in bridge mode stable for shared backend keys.
@ -8219,12 +8221,13 @@ class Router:
if deployment.litellm_params.custom_llm_provider is not None:
_model_name = deployment.litellm_params.custom_llm_provider + "/" + _model_name
# For the shared backend key, strip custom pricing fields so that
# one deployment's pricing overrides don't pollute another
# deployment sharing the same backend model name.
# Each deployment's full pricing is already stored under its
# unique model_id above (when present).
_shared_model_info = CustomPricingLiteLLMParams.strip_custom_pricing_fields(_model_info_dict)
# For the shared backend key, keep only cost-map schema fields
# (minus custom pricing) so that one deployment's pricing overrides
# or custom metadata (id, access_via_team_ids, arbitrary keys)
# don't pollute another deployment sharing the same backend model
# name. Each deployment's full model_info is already stored under
# its unique model_id above (when present).
_shared_model_info = shared_backend_model_info(_model_info_dict)
_backend_alias_cost = {_model_name: _shared_model_info}
if "responses/" in _model_name:
_stripped_model_name = _model_name.replace("responses/", "")

View file

@ -5,6 +5,7 @@ from typing import (
TYPE_CHECKING,
Any,
Dict,
FrozenSet,
List,
Literal,
Mapping,
@ -3112,6 +3113,21 @@ class CustomPricingLiteLLMParams(BaseModel):
return {k: v for k, v in model_info.items() if k not in cls.model_fields}
SHARED_BACKEND_MODEL_INFO_FIELDS: FrozenSet[str] = frozenset(
ModelInfoBase.__required_keys__ | ModelInfoBase.__optional_keys__
) - frozenset(CustomPricingLiteLLMParams.model_fields)
def shared_backend_model_info(model_info: Dict[str, Any]) -> Dict[str, Any]:
"""Return only the fields safe to register under a shared ``{provider}/{model}``
key in ``litellm.model_cost``: cost-map schema fields (``ModelInfoBase``) minus
per-deployment pricing overrides. Per-deployment metadata (``id``,
``access_via_team_ids``, arbitrary custom keys) never belongs on the shared key;
it stays under the deployment's unique model id.
"""
return {k: v for k, v in model_info.items() if k in SHARED_BACKEND_MODEL_INFO_FIELDS}
# Server-controlled fields that bound or drive an interceptor's agentic loop
# (depth, cycle fingerprints, ceiling, code-interpreter sandbox state). Listed
# in all_litellm_params so they are treated as LiteLLM-level and excluded from

View file

@ -683,6 +683,144 @@ def test_custom_pricing_isolated_from_sibling_via_proxy_model_info_path():
_restore_model_cost_entries(model_keys)
def test_custom_model_info_metadata_not_leaked_to_shared_backend_key():
"""LIT-4544: two deployments share the same backend model but carry
different custom model_info (arbitrary keys, access_via_team_ids, ids).
None of that per-deployment metadata may land on the shared backend key in
litellm.model_cost (served raw by /public/litellm_model_cost_map);
before the fix it was merged last-write-wins so values flipped randomly.
"""
backend_model = "openai/gpt-4o-mini"
shared_keys = ("gpt-4o-mini", backend_model)
leak_fields = ("id", "additionalProp1", "access_via_team_ids", "db_model")
model_keys = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (*shared_keys, "lit4544-deploy-a", "lit4544-deploy-b")
}
try:
Router(
model_list=[
{
"model_name": "alias-unrestricted",
"litellm_params": {
"model": backend_model,
"api_key": "fake-key-a",
},
"model_info": {
"id": "lit4544-deploy-a",
"additionalProp1": {"restricted": False, "model_location": "EU"},
},
},
{
"model_name": "alias-restricted",
"litellm_params": {
"model": backend_model,
"api_key": "fake-key-b",
},
"model_info": {
"id": "lit4544-deploy-b",
"additionalProp1": {"restricted": True, "model_location": "US"},
"access_via_team_ids": ["team-b-only"],
},
},
],
)
for shared_key in shared_keys:
shared_entry = litellm.model_cost.get(shared_key) or {}
leaked = [field for field in leak_fields if field in shared_entry]
assert not leaked, (
f"per-deployment metadata {leaked} leaked onto shared key "
f"{shared_key}: {shared_entry}"
)
entry_a = litellm.model_cost["lit4544-deploy-a"]
assert entry_a["additionalProp1"] == {"restricted": False, "model_location": "EU"}
entry_b = litellm.model_cost["lit4544-deploy-b"]
assert entry_b["additionalProp1"] == {"restricted": True, "model_location": "US"}
assert entry_b["access_via_team_ids"] == ["team-b-only"]
finally:
_restore_model_cost_entries(model_keys)
def test_add_deployment_does_not_leak_custom_metadata_to_shared_backend_key():
"""LIT-4544 dynamic path: deployments added at runtime (e.g. loaded from
the DB every scheduler cycle) must not re-pollute the shared backend key
with per-deployment metadata either.
"""
backend_model = "openai/gpt-4o-mini"
shared_keys = ("gpt-4o-mini", backend_model)
deploy_id = "lit4544-add-deployment"
model_keys = {
key: copy.deepcopy(litellm.model_cost.get(key))
for key in (*shared_keys, deploy_id)
}
try:
router = Router(model_list=[])
router.add_deployment(
deployment=Deployment(
model_name="alias-dynamic",
litellm_params=LiteLLM_Params(
model=backend_model,
api_key="fake-key-dynamic",
),
model_info=ModelInfo(
id=deploy_id,
additionalProp1={"restricted": True},
access_via_team_ids=["team-dynamic"],
),
)
)
for shared_key in shared_keys:
shared_entry = litellm.model_cost.get(shared_key) or {}
leaked = [
field
for field in ("id", "additionalProp1", "access_via_team_ids", "db_model")
if field in shared_entry
]
assert not leaked, (
f"per-deployment metadata {leaked} leaked onto shared key "
f"{shared_key}: {shared_entry}"
)
assert litellm.model_cost[deploy_id]["access_via_team_ids"] == ["team-dynamic"]
finally:
_restore_model_cost_entries(model_keys)
def test_shared_backend_model_info_keeps_schema_fields_and_drops_the_rest():
"""Unit test of the whitelist helper: cost-map schema fields survive,
custom pricing overrides and per-deployment metadata do not.
"""
from litellm.types.utils import shared_backend_model_info
filtered = shared_backend_model_info(
{
"mode": "chat",
"litellm_provider": "openai",
"max_tokens": 128000,
"supports_vision": True,
"input_cost_per_token": 0.99,
"output_cost_per_token": 0.99,
"id": "deploy-a",
"db_model": False,
"access_via_team_ids": ["team-a"],
"additionalProp1": {"restricted": True},
"base_model": "gpt-4o-mini",
}
)
assert filtered == {
"mode": "chat",
"litellm_provider": "openai",
"max_tokens": 128000,
"supports_vision": True,
}
def test_wildcard_zero_cost_request_does_not_poison_named_deployment_pricing():
"""LIT-3991 end to end: a proxy has a named text-embedding-3-small
deployment relying on built-in pricing plus an ``openai/*`` wildcard with