Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
38 changes: 24 additions & 14 deletions litellm/integrations/custom_guardrail.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,26 +255,31 @@ def _validate_event_hook_list_is_in_supported_event_hooks(
f"Event hook {event_hook} is not in the supported event hooks {supported_event_hooks}"
)

@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 {**team_meta, **key_meta}

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
return self._get_admin_metadata(data).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")
value = self._get_admin_metadata(data).get("opted_out_global_guardrails")
return value if isinstance(value, list) else []

def _is_valid_response_type(self, result: Any) -> bool:
Expand Down Expand Up @@ -417,7 +422,9 @@ def should_run_guardrail(
"""
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,
Expand All @@ -426,7 +433,10 @@ def should_run_guardrail(
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def _raise_env_reference_error(param: str, *, source: str) -> None:
"braintrust_host",
"slack_webhook_url",
"lunary_public_key",
"turn_off_message_logging",
]


Expand Down
40 changes: 25 additions & 15 deletions litellm/proxy/litellm_pre_call_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def _sanitize_for_log(value: Any) -> str:
text = repr(value)
# Strip CR/LF characters commonly used for log injection
return text.replace("\r", "").replace("\n", "")


from litellm.router import Router
from litellm.secret_managers.main import get_secret_bool
from litellm.types.llms.anthropic import ANTHROPIC_API_HEADERS
Expand Down Expand Up @@ -220,12 +222,12 @@ def _get_dynamic_logging_metadata(
user_api_key_dict: UserAPIKeyAuth, proxy_config: ProxyConfig
) -> Optional[TeamCallbackMetadata]:
callback_settings_obj: Optional[TeamCallbackMetadata] = None
key_dynamic_logging_settings: Optional[
dict
] = KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict)
team_dynamic_logging_settings: Optional[
dict
] = KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict)
key_dynamic_logging_settings: Optional[dict] = (
KeyAndTeamLoggingSettings.get_key_dynamic_logging_settings(user_api_key_dict)
)
team_dynamic_logging_settings: Optional[dict] = (
KeyAndTeamLoggingSettings.get_team_dynamic_logging_settings(user_api_key_dict)
)
#########################################################################################
# Key-based callbacks
#########################################################################################
Expand Down Expand Up @@ -779,11 +781,11 @@ def add_key_level_controls(

## KEY-LEVEL SPEND LOGS / TAGS
if "tags" in key_metadata and key_metadata["tags"] is not None:
data[_metadata_variable_name][
"tags"
] = LiteLLMProxyRequestSetup._merge_tags(
request_tags=data[_metadata_variable_name].get("tags"),
tags_to_add=key_metadata["tags"],
data[_metadata_variable_name]["tags"] = (
LiteLLMProxyRequestSetup._merge_tags(
request_tags=data[_metadata_variable_name].get("tags"),
tags_to_add=key_metadata["tags"],
)
)
if "disable_global_guardrails" in key_metadata and isinstance(
key_metadata["disable_global_guardrails"], bool
Expand Down Expand Up @@ -959,6 +961,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
Expand Down Expand Up @@ -1079,9 +1087,9 @@ async def add_litellm_data_to_request( # noqa: PLR0915
data[_metadata_variable_name]["litellm_api_version"] = version

if general_settings is not None:
data[_metadata_variable_name][
"global_max_parallel_requests"
] = general_settings.get("global_max_parallel_requests", None)
data[_metadata_variable_name]["global_max_parallel_requests"] = (
general_settings.get("global_max_parallel_requests", None)
)

### KEY-LEVEL Controls
key_metadata = user_api_key_dict.metadata
Expand Down Expand Up @@ -1881,7 +1889,9 @@ async def move_guardrails_to_metadata(
)

# Only check policy engine if no local config (avoid import + registry lookup)
if not (has_key_config or has_team_config or has_project_config or has_request_config):
if not (
has_key_config or has_team_config or has_project_config or has_request_config
):
from litellm.proxy.policy_engine.policy_registry import get_policy_registry

if not get_policy_registry().is_initialized():
Expand Down
46 changes: 31 additions & 15 deletions litellm/router_strategy/budget_limiter.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
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,
)
Expand Down Expand Up @@ -100,9 +103,9 @@ def __init__(
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()
Expand Down Expand Up @@ -175,7 +178,10 @@ async def async_filter_deployments(
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 {}
),
),
)

Expand Down Expand Up @@ -304,6 +310,16 @@ async def _async_get_cache_keys_for_router_budget_limiting(
deployment_configs: Dict[str, GenericBudgetInfo] = {}
deployment_providers: List[Optional[str]] = []

# Resolve tags once before the loop (loop-invariant)
_request_tags: List[str] = []
if self.tag_budget_config:
_request_tags = _get_tags_from_request_kwargs(
request_kwargs=request_kwargs,
metadata_variable_name=get_metadata_variable_name_from_kwargs(
request_kwargs or {}
),
)

for deployment in healthy_deployments:
# Check provider budgets
if self.provider_budget_config:
Expand All @@ -330,17 +346,14 @@ async def _async_get_cache_keys_for_router_budget_limiting(
cache_keys.append(
f"deployment_spend:{model_id}:{budget_config.budget_duration}"
)
# Check tag budgets
if self.tag_budget_config:
request_tags = _get_tags_from_request_kwargs(
request_kwargs=request_kwargs

# Check tag budgets (outside loop — tags are per-request, not per-deployment)
for _tag in _request_tags:
_tag_budget_config = self._get_budget_config_for_tag(_tag)
if _tag_budget_config:
cache_keys.append(
f"tag_spend:{_tag}:{_tag_budget_config.budget_duration}"
)
for _tag in request_tags:
_tag_budget_config = self._get_budget_config_for_tag(_tag)
if _tag_budget_config:
cache_keys.append(
f"tag_spend:{_tag}:{_tag_budget_config.budget_duration}"
)
return (
cache_keys,
provider_configs,
Expand Down Expand Up @@ -459,7 +472,10 @@ async def async_log_success_event(self, kwargs, response_obj, start_time, end_ti
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)
Expand Down
12 changes: 5 additions & 7 deletions litellm/router_strategy/tag_based_routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,11 @@ def _match_deployment(
return {"matched_via": "tags", "matched_value": matched_value}

# 2. Regex match against request headers.
# When match_any=False and the deployment has both plain tags and tag_regex,
# 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)
)
# When match_any=False and the deployment has plain tags, the strict tag
# check either didn't run (no request tags) or failed (step 1 returned
# None). Block the regex path so it cannot circumvent the operator's
# strict-tag policy.
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
Expand Down
Loading
Loading