Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion litellm/proxy/auth/auth_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,20 @@ async def common_checks(
)

if valid_token is not None:
from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup
from litellm.proxy.litellm_pre_call_utils import (
LITELLM_METADATA_ROUTES,
LiteLLMProxyRequestSetup,
)

# GH#30629: routes in LITELLM_METADATA_ROUTES track tags in
# litellm_metadata, but that field isn't seeded until the pre-call
# phase runs after auth. Pre-seed it here so apply_key_tags_pre_auth
# writes tags into litellm_metadata instead of leaking them into the
# provider-facing metadata field
if any(
metadata_route in route for metadata_route in LITELLM_METADATA_ROUTES
):
request_body.setdefault("litellm_metadata", {})
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved

LiteLLMProxyRequestSetup.apply_key_tags_pre_auth(
request_data=request_body,
Expand Down
4 changes: 2 additions & 2 deletions litellm/proxy/litellm_pre_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def parse_cache_control(cache_control):

LITELLM_METADATA_ROUTES = (
"batches",
"bedrock",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not add all pass throughs endpoints here ?

@mateo-berri mateo-berri Jun 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bedrock is the only passthrough that needs to be in LITELLM_METADATA_ROUTES. Every other passthrough goes through create_pass_through_route, which already strips both metadata and litellm_metadata from the forwarded body, so tags never leak there

Only Bedrock uses the body-preserving base_passthrough_process_llm_request path that leaks

Adding them all would also be unsafe, since the /openai substring would catch the native /openai/deployments/.../chat/completions route and wrongly flip its metadata field

They follow different paths because Bedrock uses the heavier allm_passthrough_route path (for SigV4 signing, URL building, router-model resolution, cost tracking) which signs and forwards the body verbatim, no strip

"/v1/messages",
"responses",
"files",
Expand Down Expand Up @@ -1468,8 +1469,7 @@ async def add_litellm_data_to_request(
_metadata_variable_name=_metadata_variable_name,
)

# Add headers to metadata for guardrails to access (fixes #17477)
# Guardrails use metadata["headers"] to access request headers (e.g., User-Agent)
# Expose request headers under the metadata field for guardrails (fixes #17477)
if _metadata_variable_name in data and isinstance(
data[_metadata_variable_name], dict
):
Expand Down
54 changes: 54 additions & 0 deletions tests/test_litellm/proxy/auth/test_auth_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1750,6 +1750,60 @@ async def test_reject_clientside_metadata_tags_allows_key_tags_without_client_ta
assert request_body["metadata"]["tags"] == ["engineering"]


@pytest.mark.asyncio
@pytest.mark.parametrize(
"route",
[
"/bedrock/model/us.anthropic.claude-sonnet-4-6/invoke",
"/v1/messages",
],
)
async def test_common_checks_metadata_route_keeps_key_tags_out_of_provider_metadata(
route,
):
"""GH#30629: on routes that track tags in litellm_metadata (bedrock, /v1/messages,
responses, ...) key-level tags must land in litellm_metadata, never in the
provider-facing metadata field (Bedrock rejects non-user_id metadata with HTTP 400).
The auth-time pre-seed keys off LITELLM_METADATA_ROUTES, so hardcoding a single route
or dropping the pre-seed makes apply_key_tags_pre_auth fall back to metadata; this
guards that regression.
"""
from fastapi import Request

from litellm.proxy.auth.auth_checks import common_checks

request_body = {"messages": [{"role": "user", "content": "test"}]}

mock_request = MagicMock(spec=Request)
valid_token = UserAPIKeyAuth(
token="test-token",
metadata={"tags": ["engineering"]},
)

with patch(
"litellm.proxy.auth.auth_checks.get_tag_objects_batch",
new_callable=AsyncMock,
return_value={},
):
result = await common_checks(
request_body=request_body,
team_object=None,
user_object=None,
end_user_object=None,
global_proxy_spend=None,
general_settings={},
route=route,
llm_router=None,
proxy_logging_obj=MagicMock(),
valid_token=valid_token,
request=mock_request,
)

assert result is True
assert request_body["litellm_metadata"]["tags"] == ["engineering"]
assert "metadata" not in request_body


@pytest.mark.asyncio
async def test_virtual_key_soft_budget_check_with_user_obj():
"""Test _virtual_key_soft_budget_check includes user_email when user_obj is provided"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2682,15 +2682,19 @@ async def test_add_litellm_data_to_request_adds_headers_to_metadata():
version="1.0",
)

# Verify headers are added to metadata for guardrails
assert "metadata" in result, "metadata should be present in result"
assert "headers" in result["metadata"], "headers should be present in metadata"
# Verify headers are added to litellm_metadata for guardrails.
# Bedrock passthrough uses litellm_metadata to prevent key-level
# tags from leaking into the provider payload (GH#30629).
assert "litellm_metadata" in result, "litellm_metadata should be present in result"
assert (
"headers" in result["litellm_metadata"]
), "headers should be present in litellm_metadata"
assert isinstance(
result["metadata"]["headers"], dict
result["litellm_metadata"]["headers"], dict
), "headers should be a dictionary"

# Verify specific headers are accessible (important for guardrails)
headers = result["metadata"]["headers"]
headers = result["litellm_metadata"]["headers"]
assert (
"user-agent" in headers or "User-Agent" in headers
), "User-Agent header should be accessible in metadata"
Expand Down
10 changes: 10 additions & 0 deletions tests/test_litellm/proxy/test_litellm_pre_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ def test_returns_metadata_for_embeddings(self):
request = self._make_request("/v1/embeddings")
assert _get_metadata_variable_name(request) == "metadata"

def test_returns_litellm_metadata_for_bedrock_invoke(self):
# GH#30629: bedrock passthrough must use litellm_metadata
# to prevent key-level tags from leaking into provider body
request = self._make_request("/bedrock/model/us.anthropic.claude-sonnet-4-6/invoke")
assert _get_metadata_variable_name(request) == "litellm_metadata"

def test_returns_litellm_metadata_for_bedrock_converse(self):
request = self._make_request("/bedrock/model/us.anthropic.claude-sonnet-4-6/converse")
assert _get_metadata_variable_name(request) == "litellm_metadata"


def test_get_enforced_params_for_service_account_settings():
"""
Expand Down
Loading