diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 52f7f227b52..5b669bd048f 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -49,7 +49,9 @@ ProxyConfig = _ProxyConfig else: ProxyConfig = Any -from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request +from litellm.proxy.litellm_pre_call_utils import ( + add_litellm_data_to_request, +) from litellm.types.utils import ModelResponse, ModelResponseStream, Usage diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index 3f844f21eb0..ad0ab6b7a38 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -846,7 +846,9 @@ async def add_litellm_data_to_request( # noqa: PLR0915 # Add headers to metadata for guardrails to access (fixes #17477) # Guardrails use metadata["headers"] to access request headers (e.g., User-Agent) - if _metadata_variable_name in data and isinstance(data[_metadata_variable_name], dict): + if _metadata_variable_name in data and isinstance( + data[_metadata_variable_name], dict + ): data[_metadata_variable_name]["headers"] = _headers # check for forwardable headers @@ -1307,6 +1309,9 @@ def move_guardrails_to_metadata( - If guardrails set on API Key metadata then sets guardrails on request metadata - If guardrails not set on API key, then checks request metadata + + Note: We copy (not pop) guardrails from data to metadata to ensure deployment-level + guardrails merged by the router remain in kwargs for async_pre_call_deployment_hook. """ # Check key-level guardrails _add_guardrails_from_key_or_team_metadata( @@ -1319,15 +1324,25 @@ def move_guardrails_to_metadata( ######################################################################################### # User's might send "guardrails" in the request body, we need to add them to the request metadata. # Since downstream logic requires "guardrails" to be in the request metadata + # + # IMPORTANT: We copy instead of pop to preserve guardrails in kwargs for + # async_pre_call_deployment_hook (custom_guardrail.py:290) which checks kwargs.get("guardrails"). + # This is the event-based approach for deployment-level guardrails. ######################################################################################### if "guardrails" in data: - request_body_guardrails = data.pop("guardrails") + request_body_guardrails = data.get("guardrails") + if request_body_guardrails is None: + return if "guardrails" in data[_metadata_variable_name] and isinstance( data[_metadata_variable_name]["guardrails"], list ): - data[_metadata_variable_name]["guardrails"].extend(request_body_guardrails) + # Merge unique guardrails + existing = data[_metadata_variable_name]["guardrails"] + for g in request_body_guardrails: + if g not in existing: + existing.append(g) else: - data[_metadata_variable_name]["guardrails"] = request_body_guardrails + data[_metadata_variable_name]["guardrails"] = list(request_body_guardrails) ######################################################################################### if "guardrail_config" in data: