From 22572eafaf3d59caec1da10b8cf9fc073abb5a29 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 16 Apr 2026 21:29:13 +0000 Subject: [PATCH] fix: merge admin metadata from both metadata and litellm_metadata Greptile P2: _get_admin_metadata used 'litellm_metadata or metadata', meaning a caller sending a non-empty litellm_metadata would shadow admin config the proxy had injected into data['metadata']. Admin exemptions would be silently ignored. Check both keys and prefer whichever contains admin fields. Add regression test covering the shadowing scenario. --- litellm/integrations/custom_guardrail.py | 19 ++++++++++++++----- .../integrations/test_custom_guardrail.py | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 89431847e9..b0931964cc 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -257,11 +257,20 @@ class CustomGuardrail(CustomLogger): @staticmethod def _get_admin_metadata(data: dict) -> dict: - """Return merged admin-configured key and team metadata from the request data.""" - metadata = data.get("litellm_metadata") or data.get("metadata", {}) - team_meta = metadata.get("user_api_key_team_metadata") or {} - key_meta = metadata.get("user_api_key_metadata") or {} - # Key-level settings override team-level + """Return merged admin-configured key and team metadata from the request data. + + The proxy may inject admin metadata (user_api_key_metadata, + user_api_key_team_metadata) into either ``metadata`` or + ``litellm_metadata`` depending on endpoint. Check both so a caller + cannot shadow admin config by pre-populating the other key. + Key-level settings override team-level. + """ + team_meta: dict = {} + key_meta: dict = {} + for key in ("metadata", "litellm_metadata"): + meta = data.get(key) or {} + team_meta = meta.get("user_api_key_team_metadata") or team_meta + key_meta = meta.get("user_api_key_metadata") or key_meta return {**team_meta, **key_meta} def get_disable_global_guardrail(self, data: dict) -> Optional[bool]: diff --git a/tests/test_litellm/integrations/test_custom_guardrail.py b/tests/test_litellm/integrations/test_custom_guardrail.py index 4c1ef853ab..b8eb085736 100644 --- a/tests/test_litellm/integrations/test_custom_guardrail.py +++ b/tests/test_litellm/integrations/test_custom_guardrail.py @@ -227,6 +227,20 @@ class TestCustomGuardrailShouldRunGuardrail: ) assert result is False, "Admin-configured disable should be respected" + # Test 5: Admin config in metadata isn't shadowed by user-supplied litellm_metadata + data_cross_key = { + "model": "gpt-3.5-turbo", + "messages": [{"role": "user", "content": "test"}], + "metadata": {"user_api_key_metadata": {"disable_global_guardrails": True}}, + "litellm_metadata": {"request_tags": ["user-supplied"]}, + } + result = custom_guardrail.should_run_guardrail( + data=data_cross_key, event_type=GuardrailEventHooks.pre_call + ) + assert ( + result is False + ), "Admin config in metadata must not be shadowed by user-supplied litellm_metadata" + def test_should_run_guardrail_with_opted_out_global_guardrails(self): """Test that per-guardrail opt-out only works from admin metadata""" from litellm.types.guardrails import GuardrailEventHooks