From 74a49b527c6db53bb8f89d83cdebec576589b62b Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:24:10 +0000 Subject: [PATCH] fix(proxy): read guardrail config from admin metadata, fix tag routing consistency Read guardrail control flags (disable_global_guardrails, opted_out_global_guardrails) from admin-configured key metadata instead of the request body. This ensures callers cannot override admin security policies. Fix tag-based routing to enforce strict tag checks regardless of whether the request includes tags. Fix budget limiter to use the same dynamic metadata key resolution as the tag router for consistent tag extraction. --- litellm/integrations/custom_guardrail.py | 29 ++++++++++++-------- litellm/proxy/litellm_pre_call_utils.py | 6 ++++ litellm/router_strategy/budget_limiter.py | 24 ++++++++++++---- litellm/router_strategy/tag_based_routing.py | 4 +-- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/litellm/integrations/custom_guardrail.py b/litellm/integrations/custom_guardrail.py index 6046f1bb58..1e6a46044e 100644 --- a/litellm/integrations/custom_guardrail.py +++ b/litellm/integrations/custom_guardrail.py @@ -257,24 +257,24 @@ class CustomGuardrail(CustomLogger): def get_disable_global_guardrail(self, data: dict) -> Optional[bool]: """ - Returns True if the global guardrail should be disabled + Returns True if the global guardrail should be disabled. + + Reads from admin-configured key/team metadata only, not from + the request body, to prevent callers from disabling guardrails. """ - if "disable_global_guardrails" in data: - return data["disable_global_guardrails"] metadata = data.get("litellm_metadata") or data.get("metadata", {}) - if "disable_global_guardrails" in metadata: - return metadata["disable_global_guardrails"] - return False + admin_metadata = metadata.get("user_api_key_metadata") or {} + return admin_metadata.get("disable_global_guardrails", False) def get_opted_out_global_guardrails_from_metadata(self, data: dict) -> List[str]: """ Returns the list of global guardrail names the team/key has opted out of. + + Reads from admin-configured key/team metadata only. """ - if "opted_out_global_guardrails" in data: - value = data["opted_out_global_guardrails"] - return value if isinstance(value, list) else [] metadata = data.get("litellm_metadata") or data.get("metadata", {}) - value = metadata.get("opted_out_global_guardrails") + admin_metadata = metadata.get("user_api_key_metadata") or {} + value = admin_metadata.get("opted_out_global_guardrails") return value if isinstance(value, list) else [] def _is_valid_response_type(self, result: Any) -> bool: @@ -417,7 +417,9 @@ class CustomGuardrail(CustomLogger): """ requested_guardrails = self.get_guardrail_from_metadata(data) disable_global_guardrail = self.get_disable_global_guardrail(data) - opted_out_global_guardrails = self.get_opted_out_global_guardrails_from_metadata(data) + opted_out_global_guardrails = ( + self.get_opted_out_global_guardrails_from_metadata(data) + ) verbose_logger.debug( "inside should_run_guardrail for guardrail=%s event_type= %s guardrail_supported_event_hooks= %s requested_guardrails= %s self.default_on= %s", self.guardrail_name, @@ -426,7 +428,10 @@ class CustomGuardrail(CustomLogger): requested_guardrails, self.default_on, ) - if self.default_on is True and self.guardrail_name in opted_out_global_guardrails: + if ( + self.default_on is True + and self.guardrail_name in opted_out_global_guardrails + ): return False if self.default_on is True and disable_global_guardrail is not True: diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index b0adf7aa6e..95e1cbed44 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -977,6 +977,12 @@ async def add_litellm_data_to_request( # noqa: PLR0915 "Setting client-provided x-api-key as api_key parameter (will override deployment key)" ) + # Strip internal pipeline state from user input + for _meta_key in ("metadata", "litellm_metadata"): + _user_meta = data.get(_meta_key) + if isinstance(_user_meta, dict): + _user_meta.pop("_pipeline_managed_guardrails", None) + ########################################################## # Init - Proxy Server Request # we do this as soon as entering so we track the original request diff --git a/litellm/router_strategy/budget_limiter.py b/litellm/router_strategy/budget_limiter.py index 64dc5fe474..261e659644 100644 --- a/litellm/router_strategy/budget_limiter.py +++ b/litellm/router_strategy/budget_limiter.py @@ -29,6 +29,9 @@ from litellm.caching.redis_cache import RedisPipelineIncrementOperation from litellm.integrations.custom_logger import CustomLogger, Span from litellm.litellm_core_utils.duration_parser import duration_in_seconds from litellm.router_strategy.tag_based_routing import _get_tags_from_request_kwargs +from litellm.litellm_core_utils.core_helpers import ( + get_metadata_variable_name_from_kwargs, +) from litellm.router_utils.cooldown_callbacks import ( _get_prometheus_logger_from_callbacks, ) @@ -100,9 +103,9 @@ class RouterBudgetLimiting(CustomLogger): self.dual_cache = dual_cache self.redis_increment_operation_queue: List[RedisPipelineIncrementOperation] = [] asyncio.create_task(self.periodic_sync_in_memory_spend_with_redis()) - self.provider_budget_config: Optional[ - GenericBudgetConfigType - ] = provider_budget_config + self.provider_budget_config: Optional[GenericBudgetConfigType] = ( + provider_budget_config + ) self.deployment_budget_config: Optional[GenericBudgetConfigType] = None self.tag_budget_config: Optional[GenericBudgetConfigType] = None self._init_provider_budgets() @@ -175,7 +178,10 @@ class RouterBudgetLimiting(CustomLogger): spend_map=spend_map, potential_deployments=potential_deployments, request_tags=_get_tags_from_request_kwargs( - request_kwargs=request_kwargs + request_kwargs=request_kwargs, + metadata_variable_name=get_metadata_variable_name_from_kwargs( + request_kwargs or {} + ), ), ) @@ -333,7 +339,10 @@ class RouterBudgetLimiting(CustomLogger): # Check tag budgets if self.tag_budget_config: request_tags = _get_tags_from_request_kwargs( - request_kwargs=request_kwargs + request_kwargs=request_kwargs, + metadata_variable_name=get_metadata_variable_name_from_kwargs( + request_kwargs or {} + ), ) for _tag in request_tags: _tag_budget_config = self._get_budget_config_for_tag(_tag) @@ -459,7 +468,10 @@ class RouterBudgetLimiting(CustomLogger): response_cost=response_cost, ) - request_tags = _get_tags_from_request_kwargs(kwargs) + request_tags = _get_tags_from_request_kwargs( + kwargs, + metadata_variable_name=get_metadata_variable_name_from_kwargs(kwargs or {}), + ) if len(request_tags) > 0: for _tag in request_tags: _tag_budget_config = self._get_budget_config_for_tag(_tag) diff --git a/litellm/router_strategy/tag_based_routing.py b/litellm/router_strategy/tag_based_routing.py index 1188ce9d59..b0b154fc77 100644 --- a/litellm/router_strategy/tag_based_routing.py +++ b/litellm/router_strategy/tag_based_routing.py @@ -106,9 +106,7 @@ def _match_deployment( # the strict tag check has already failed (step 1 returned None). Allow # the regex to fire only when the deployment has NO plain tags, so we never # use regex as a backdoor around the operator's strict-tag policy. - strict_tag_check_failed = ( - not match_any and bool(deployment_tags) and bool(request_tags) - ) + strict_tag_check_failed = not match_any and bool(deployment_tags) if deployment_tag_regex and header_strings and not strict_tag_check_failed: regex_match = _is_valid_deployment_tag_regex( deployment_tag_regex, header_strings