fix: keep body tags visible to _tag_max_budget_check pre-auth

Stripping body-supplied tags in apply_client_tag_policy_pre_auth silently
disabled per-tag budget enforcement for non-opted-in keys — pre-PR
behavior was that those tags reached _tag_max_budget_check inside
common_checks. The post-auth strip in add_litellm_data_to_request
continues to remove unauthorized tags before they leave the proxy.

Also moves the LiteLLMProxyRequestSetup import to module-level (no
circular dep with auth/user_api_key_auth.py).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
shivam 2026-05-09 18:29:23 -07:00
parent 36caeb013b
commit 4fc0f1d8f6
No known key found for this signature in database
3 changed files with 22 additions and 20 deletions

View file

@ -67,6 +67,7 @@ from litellm.proxy.common_utils.http_parsing_utils import (
populate_request_with_path_params,
)
from litellm.proxy.common_utils.realtime_utils import _realtime_request_body
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
from litellm.proxy.utils import (
PrismaClient,
ProxyLogging,
@ -1939,13 +1940,10 @@ async def _run_centralized_common_checks(
llm_router=llm_router,
)
# Merge x-litellm-tags (or strip body tags when the key/team has not
# opted in via allow_client_tags) into request_data BEFORE common_checks
# runs. _tag_max_budget_check inside common_checks only inspects
# request_data; without this pre-merge, header-supplied tags bypass
# tag-budget enforcement.
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
# Merge x-litellm-tags into request_data BEFORE common_checks runs.
# _tag_max_budget_check inside common_checks only inspects request_data;
# without this pre-merge, header-supplied tags bypass tag-budget
# enforcement.
LiteLLMProxyRequestSetup.apply_client_tag_policy_pre_auth(
request=request,
request_data=request_data,

View file

@ -1217,15 +1217,12 @@ class LiteLLMProxyRequestSetup:
break
if not _admin_allow_client_tags:
# Strip any caller-supplied tags so the budget gate doesn't act
# on tags this key/team isn't authorized to set. Matches the
# post-auth strip in add_litellm_data_to_request.
for _meta_key in ("metadata", "litellm_metadata"):
_user_meta = request_data.get(_meta_key)
if isinstance(_user_meta, dict) and "tags" in _user_meta:
_user_meta.pop("tags", None)
if "tags" in request_data:
request_data.pop("tags", None)
# Don't strip body-supplied tags here — pre-PR behavior was that
# _tag_max_budget_check (inside common_checks) saw and enforced
# per-tag budgets on body tags regardless of allow_client_tags.
# Stripping pre-auth would silently disable that enforcement.
# The post-auth strip in add_litellm_data_to_request still
# removes unauthorized tags before they leave the proxy.
return
headers = _safe_get_request_headers(request=request)

View file

@ -4110,7 +4110,14 @@ class TestApplyClientTagPolicyPreAuth:
# Existing tags first, dedupe header tags
assert data["metadata"]["tags"] == ["env:prod", "team:platform", "tenant:acme"]
def test_strips_body_tags_when_not_opted_in(self):
def test_preserves_body_tags_when_not_opted_in(self):
# Pre-auth must NOT strip body-supplied tags for non-opted-in keys.
# _tag_max_budget_check (inside common_checks) enforces per-tag
# budgets on whatever tags it sees in request_data, and pre-PR
# behavior was that body tags hit that check regardless of
# allow_client_tags. The post-auth strip in add_litellm_data_to_request
# cleans them up before they leave the proxy — that's covered by a
# separate regression test.
request_mock = _build_request_mock_with_headers(
{"x-litellm-tags": "tenant:acme"}
)
@ -4132,9 +4139,9 @@ class TestApplyClientTagPolicyPreAuth:
user_api_key_dict=user_api_key_dict,
)
assert "tags" not in data
assert "tags" not in data["metadata"]
assert "tags" not in data["litellm_metadata"]
assert data["tags"] == ["root-tag"]
assert data["metadata"]["tags"] == ["meta-tag"]
assert data["litellm_metadata"]["tags"] == ["litellm-meta-tag"]
def test_does_not_merge_header_tags_when_not_opted_in(self):
# Even with the header set, no opt-in means the header is ignored