diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index aa3a29e4f2f..7467bbae232 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1132,6 +1132,13 @@ async def add_litellm_data_to_request( # noqa: PLR0915 if isinstance(_user_meta, dict) and "tags" in _user_meta: _user_meta.pop("tags", None) _stripped_from.append(_meta_key) + # Also strip the root-level `tags` field. get_tags_from_request_body + # reads request_body["tags"] directly and feeds it to the policy + # engine, so leaving it in place here would let the strip-in-metadata + # above be trivially bypassed by moving the tags to the body root. + if "tags" in data: + data.pop("tags", None) + _stripped_from.append("tags (root)") if _stripped_from: verbose_proxy_logger.warning( "Stripped caller-supplied tags from %s: this key/team does " diff --git a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py index 664a936b540..360ddd426a4 100644 --- a/tests/test_litellm/proxy/test_litellm_pre_call_utils.py +++ b/tests/test_litellm/proxy/test_litellm_pre_call_utils.py @@ -596,6 +596,11 @@ async def test_add_litellm_data_to_request_ignores_root_level_tags_without_permi ) assert "tags" not in (updated.get("metadata") or {}) + # Also ensure the root-level tags are removed. get_tags_from_request_body + # reads request_body["tags"] directly, so leaving it in place would let + # the policy engine see caller-supplied tags even after the metadata + # strip. + assert "tags" not in updated @pytest.mark.asyncio