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