[Feat] Guardrails Load Balancing - Allow Platform admins to load balance between guardrails (#18181)

* add _aguardrail_helper for LB

* add _aguardrail_helper on router.py

* test_proxy_logging_pre_call_hook_load_balancing

* add _execute_guardrail_with_load_balancing

* add LB TEsting

* docs guard lb

* fix linting

* fix lint
This commit is contained in:
Ishaan Jaff 2025-12-19 00:08:03 +05:30 committed by GitHub
parent fcd524ca0f
commit 5ea0854eda
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1039 additions and 21 deletions

View File

@ -0,0 +1,351 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Guardrail Load Balancing
Load balance guardrail requests across multiple guardrail deployments. This is useful when you have rate limits on guardrail providers (e.g., AWS Bedrock Guardrails) and want to distribute requests across multiple accounts or regions.
## How It Works
```mermaid
flowchart LR
subgraph LiteLLM Gateway
Router[Router]
G1[Guardrail Instance A]
G2[Guardrail Instance B]
G3[Guardrail Instance N]
end
Client[Client Request] --> Router
Router -->|Round Robin / Weighted| G1
Router -->|Round Robin / Weighted| G2
Router -->|Round Robin / Weighted| G3
G1 --> AWS1[AWS Account 1]
G2 --> AWS2[AWS Account 2]
G3 --> AWSN[AWS Account N]
```
When you define multiple guardrails with the **same `guardrail_name`**, LiteLLM automatically load balances requests across them using the router's load balancing strategy.
## Why Use Guardrail Load Balancing?
| Use Case | Benefit |
|----------|---------|
| **AWS Bedrock Rate Limits** | Bedrock Guardrails have per-account rate limits. Distribute across multiple AWS accounts to increase throughput |
| **Multi-Region Redundancy** | Deploy guardrails across regions for failover and lower latency |
| **Cost Optimization** | Spread usage across accounts with different pricing tiers or credits |
| **A/B Testing** | Test different guardrail configurations with weighted distribution |
## Quick Start
### 1. Define Multiple Guardrails with Same Name
Define multiple guardrail entries with the **same `guardrail_name`** but different configurations:
<Tabs>
<TabItem value="bedrock" label="Bedrock Guardrails">
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: gpt-4
litellm_params:
model: openai/gpt-4
api_key: os.environ/OPENAI_API_KEY
guardrails:
# First Bedrock guardrail - AWS Account 1
- guardrail_name: "content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "pre_call"
guardrailIdentifier: "abc123"
guardrailVersion: "1"
aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID_1
aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY_1
aws_region_name: "us-east-1"
# Second Bedrock guardrail - AWS Account 2
- guardrail_name: "content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "pre_call"
guardrailIdentifier: "def456"
guardrailVersion: "1"
aws_access_key_id: os.environ/AWS_ACCESS_KEY_ID_2
aws_secret_access_key: os.environ/AWS_SECRET_ACCESS_KEY_2
aws_region_name: "us-west-2"
```
</TabItem>
<TabItem value="custom" label="Custom Guardrails">
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: gpt-4
litellm_params:
model: openai/gpt-4
api_key: os.environ/OPENAI_API_KEY
guardrails:
# First custom guardrail instance
- guardrail_name: "pii-filter"
litellm_params:
guardrail: custom_guardrail.PIIFilterA
mode: "pre_call"
# Second custom guardrail instance
- guardrail_name: "pii-filter"
litellm_params:
guardrail: custom_guardrail.PIIFilterB
mode: "pre_call"
```
</TabItem>
<TabItem value="aporia" label="Aporia Guardrails">
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: gpt-4
litellm_params:
model: openai/gpt-4
api_key: os.environ/OPENAI_API_KEY
guardrails:
# First Aporia instance
- guardrail_name: "toxicity-filter"
litellm_params:
guardrail: aporia
mode: "pre_call"
api_key: os.environ/APORIA_API_KEY_1
api_base: os.environ/APORIA_API_BASE_1
# Second Aporia instance
- guardrail_name: "toxicity-filter"
litellm_params:
guardrail: aporia
mode: "pre_call"
api_key: os.environ/APORIA_API_KEY_2
api_base: os.environ/APORIA_API_BASE_2
```
</TabItem>
</Tabs>
### 2. Start LiteLLM Gateway
```bash showLineNumbers title="Start proxy"
litellm --config config.yaml --detailed_debug
```
### 3. Make Requests
Requests using the guardrail will be automatically load balanced:
```bash showLineNumbers title="Test request"
curl -X POST http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-1234" \
-d '{
"model": "gpt-4",
"messages": [{"role": "user", "content": "Hello, how are you?"}],
"guardrails": ["content-filter"]
}'
```
## Weighted Load Balancing
Assign weights to distribute traffic unevenly across guardrail instances:
```yaml showLineNumbers title="config.yaml - Weighted distribution"
guardrails:
# 80% of traffic
- guardrail_name: "content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "pre_call"
guardrailIdentifier: "primary-guard"
guardrailVersion: "1"
weight: 8 # Higher weight = more traffic
# 20% of traffic
- guardrail_name: "content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "pre_call"
guardrailIdentifier: "secondary-guard"
guardrailVersion: "1"
weight: 2 # Lower weight = less traffic
```
## Bedrock Guardrails - Multi-Account Setup
AWS Bedrock Guardrails have rate limits per account. Here's how to set up load balancing across multiple AWS accounts:
### Architecture
```mermaid
flowchart TB
subgraph LiteLLM["LiteLLM Gateway"]
LB[Load Balancer]
end
subgraph AWS1["AWS Account 1 (us-east-1)"]
BG1[Bedrock Guardrail]
end
subgraph AWS2["AWS Account 2 (us-west-2)"]
BG2[Bedrock Guardrail]
end
subgraph AWS3["AWS Account 3 (eu-west-1)"]
BG3[Bedrock Guardrail]
end
Client[Client] --> LiteLLM
LB --> BG1
LB --> BG2
LB --> BG3
```
### Configuration
```yaml showLineNumbers title="config.yaml - Multi-account Bedrock"
model_list:
- model_name: claude-3
litellm_params:
model: bedrock/anthropic.claude-3-sonnet-20240229-v1:0
guardrails:
# AWS Account 1 - US East
- guardrail_name: "bedrock-content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "during_call"
guardrailIdentifier: "guard-us-east"
guardrailVersion: "DRAFT"
aws_access_key_id: os.environ/AWS_ACCESS_KEY_1
aws_secret_access_key: os.environ/AWS_SECRET_KEY_1
aws_region_name: "us-east-1"
# AWS Account 2 - US West
- guardrail_name: "bedrock-content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "during_call"
guardrailIdentifier: "guard-us-west"
guardrailVersion: "DRAFT"
aws_access_key_id: os.environ/AWS_ACCESS_KEY_2
aws_secret_access_key: os.environ/AWS_SECRET_KEY_2
aws_region_name: "us-west-2"
# AWS Account 3 - EU West
- guardrail_name: "bedrock-content-filter"
litellm_params:
guardrail: bedrock/guardrail
mode: "during_call"
guardrailIdentifier: "guard-eu-west"
guardrailVersion: "DRAFT"
aws_access_key_id: os.environ/AWS_ACCESS_KEY_3
aws_secret_access_key: os.environ/AWS_SECRET_KEY_3
aws_region_name: "eu-west-1"
```
### Test Multi-Account Setup
```bash showLineNumbers title="Run multiple requests to verify load balancing"
# Run 10 requests - they will be distributed across accounts
for i in {1..10}; do
curl -s -X POST http://localhost:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-1234" \
-d '{
"model": "claude-3",
"messages": [{"role": "user", "content": "Hello"}],
"guardrails": ["bedrock-content-filter"]
}' &
done
wait
```
Check proxy logs to verify requests are distributed across different AWS accounts.
## Custom Guardrails Example
Create two custom guardrail classes for load balancing:
```python showLineNumbers title="custom_guardrail.py"
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.proxy._types import UserAPIKeyAuth
from litellm.caching.caching import DualCache
class PIIFilterA(CustomGuardrail):
"""PII Filter Instance A"""
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: str,
):
print("PIIFilterA processing request")
# Your PII filtering logic here
return data
class PIIFilterB(CustomGuardrail):
"""PII Filter Instance B"""
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: str,
):
print("PIIFilterB processing request")
# Your PII filtering logic here
return data
```
```yaml showLineNumbers title="config.yaml"
guardrails:
- guardrail_name: "pii-filter"
litellm_params:
guardrail: custom_guardrail.PIIFilterA
mode: "pre_call"
- guardrail_name: "pii-filter"
litellm_params:
guardrail: custom_guardrail.PIIFilterB
mode: "pre_call"
```
## Verifying Load Balancing
Enable detailed debug logging to verify load balancing is working:
```bash showLineNumbers title="Start with debug logging"
litellm --config config.yaml --detailed_debug
```
You should see logs indicating which guardrail instance is selected:
```
Selected guardrail deployment: bedrock/guardrail (guard-us-east)
Selected guardrail deployment: bedrock/guardrail (guard-us-west)
Selected guardrail deployment: bedrock/guardrail (guard-eu-west)
...
```
## Related
- [Guardrails Quick Start](./quick_start.md)
- [Bedrock Guardrails](./bedrock.md)
- [Custom Guardrails](./custom_guardrail.md)
- [Load Balancing for LLM Calls](../load_balancing.md)

View File

@ -69,6 +69,13 @@ guardrails:
- `during_call` Run **during** LLM call, on **input** Same as `pre_call` but runs in parallel as LLM call. Response not returned until guardrail check completes
- A list of the above values to run multiple modes, e.g. `mode: [pre_call, post_call]`
### Load Balancing Guardrails
Need to distribute guardrail requests across multiple accounts or regions? See [Guardrail Load Balancing](./guardrail_load_balancing.md) for details on:
- Load balancing across multiple AWS Bedrock accounts (useful for rate limit management)
- Weighted distribution across guardrail instances
- Multi-region guardrail deployments
## 2. Start LiteLLM Gateway

View File

@ -42,6 +42,7 @@ const sidebars = {
label: "Guardrails",
items: [
"proxy/guardrails/quick_start",
"proxy/guardrails/guardrail_load_balancing",
{
type: "category",
"label": "Contributing to Guardrails",

View File

@ -12354,6 +12354,7 @@
"max_videos_per_prompt": 10,
"mode": "image_generation",
"output_cost_per_image": 0.039,
"output_cost_per_image_token": 3e-05,
"output_cost_per_reasoning_token": 2.5e-06,
"output_cost_per_token": 2.5e-06,
"rpm": 100000,
@ -12402,6 +12403,7 @@
"max_videos_per_prompt": 10,
"mode": "image_generation",
"output_cost_per_image": 0.039,
"output_cost_per_image_token": 3e-05,
"output_cost_per_reasoning_token": 3e-05,
"output_cost_per_token": 3e-05,
"rpm": 100000,
@ -14134,6 +14136,7 @@
"max_videos_per_prompt": 10,
"mode": "image_generation",
"output_cost_per_image": 0.039,
"output_cost_per_image_token": 3e-05,
"output_cost_per_reasoning_token": 2.5e-06,
"output_cost_per_token": 2.5e-06,
"rpm": 100000,
@ -14182,6 +14185,7 @@
"max_videos_per_prompt": 10,
"mode": "image_generation",
"output_cost_per_image": 0.039,
"output_cost_per_image_token": 3e-05,
"output_cost_per_reasoning_token": 3e-05,
"output_cost_per_token": 3e-05,
"rpm": 100000,
@ -27288,6 +27292,7 @@
"max_videos_per_prompt": 10,
"mode": "image_generation",
"output_cost_per_image": 0.039,
"output_cost_per_image_token": 3e-05,
"output_cost_per_reasoning_token": 2.5e-06,
"output_cost_per_token": 2.5e-06,
"rpm": 100000,

View File

@ -8,6 +8,43 @@ from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.guardrails.guardrail_helpers import should_proceed_based_on_metadata
from litellm.types.utils import CallTypesLiteral
# Global counter for tracking which guardrail was called (for load balancing tests)
guardrail_lb_call_count: Dict[str, int] = {"A": 0, "B": 0}
class GuardrailForLBTestingA(CustomGuardrail):
"""Guardrail A for load balancing testing."""
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: CallTypesLiteral,
) -> Optional[Union[Exception, str, dict]]:
guardrail_lb_call_count["A"] += 1
verbose_proxy_logger.info(
f"GuardrailForLBTestingA called. Total A calls: {guardrail_lb_call_count['A']}"
)
return data
class GuardrailForLBTestingB(CustomGuardrail):
"""Guardrail B for load balancing testing."""
async def async_pre_call_hook(
self,
user_api_key_dict: UserAPIKeyAuth,
cache: DualCache,
data: dict,
call_type: CallTypesLiteral,
) -> Optional[Union[Exception, str, dict]]:
guardrail_lb_call_count["B"] += 1
verbose_proxy_logger.info(
f"GuardrailForLBTestingB called. Total B calls: {guardrail_lb_call_count['B']}"
)
return data
class myCustomGuardrail(CustomGuardrail):
def __init__(

View File

@ -78,6 +78,15 @@ guardrails:
litellm_params:
guardrail: custom_guardrail.myCustomGuardrail
mode: "post_call"
# Load balancing guardrails - two guardrails with same name
- guardrail_name: "lb-test-guard"
litellm_params:
guardrail: custom_guardrail.GuardrailForLBTestingA
mode: "pre_call"
- guardrail_name: "lb-test-guard"
litellm_params:
guardrail: custom_guardrail.GuardrailForLBTestingB
mode: "pre_call"
router_settings:
enable_tag_filtering: True # 👈 Key Change

View File

@ -1,4 +1,4 @@
from typing import Dict, List, Optional, cast
from typing import Any, Dict, List, Optional, cast
import litellm
from litellm import Router
@ -36,6 +36,67 @@ def init_guardrails_v2(
verbose_proxy_logger.debug(f"\nGuardrail List:{guardrail_list}\n")
# Populate router's guardrail_list for load balancing support
_populate_router_guardrail_list(guardrail_list=guardrail_list)
def _populate_router_guardrail_list(guardrail_list: List[Guardrail]) -> None:
"""
Populate the router's guardrail_list from initialized guardrails.
This enables load balancing across multiple guardrail deployments
with the same guardrail_name.
"""
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import llm_router
from litellm.types.router import GuardrailTypedDict
if llm_router is None:
verbose_proxy_logger.debug(
"Router not initialized yet, skipping guardrail_list population"
)
return
router_guardrail_list: List[GuardrailTypedDict] = []
for guardrail in guardrail_list:
guardrail_id = guardrail.get("guardrail_id")
guardrail_name = guardrail.get("guardrail_name")
litellm_params: Any = guardrail.get("litellm_params", {})
# Get the callback instance from the registry
callback = None
if guardrail_id:
callback = IN_MEMORY_GUARDRAIL_HANDLER.guardrail_id_to_custom_guardrail.get(
guardrail_id
)
# Build litellm_params dict for the router
params_dict = (
litellm_params.model_dump()
if hasattr(litellm_params, "model_dump")
else dict(litellm_params)
)
router_guardrail: GuardrailTypedDict = GuardrailTypedDict(
guardrail_name=guardrail_name or "",
litellm_params={
"guardrail": params_dict.get("guardrail", ""),
"mode": params_dict.get("mode", ""),
"api_key": params_dict.get("api_key"),
"api_base": params_dict.get("api_base"),
},
callback=callback,
id=guardrail_id,
)
router_guardrail_list.append(router_guardrail)
llm_router.guardrail_list = router_guardrail_list
verbose_proxy_logger.debug(
f"Populated router guardrail_list with {len(router_guardrail_list)} guardrails"
)
### LEGACY IMPLEMENTATION ###
def initialize_guardrails(

View File

@ -36,10 +36,18 @@ from litellm.types.guardrails import GuardrailEventHooks
from litellm.types.utils import CallTypes, CallTypesLiteral
try:
from litellm_enterprise.enterprise_callbacks.send_emails.base_email import BaseEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import SendGridEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import SMTPEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import ResendEmailLogger
from litellm_enterprise.enterprise_callbacks.send_emails.base_email import (
BaseEmailLogger,
)
from litellm_enterprise.enterprise_callbacks.send_emails.resend_email import (
ResendEmailLogger,
)
from litellm_enterprise.enterprise_callbacks.send_emails.sendgrid_email import (
SendGridEmailLogger,
)
from litellm_enterprise.enterprise_callbacks.send_emails.smtp_email import (
SMTPEmailLogger,
)
except ImportError:
BaseEmailLogger = None # type: ignore
SendGridEmailLogger = None # type: ignore
@ -813,6 +821,125 @@ class ProxyLogging:
raise HTTPException(status_code=400, detail={"error": response})
return data
def _should_use_guardrail_load_balancing(
self,
guardrail_name: str,
) -> bool:
"""
Check if load balancing should be used for this guardrail.
Returns True if the router has multiple deployments for this guardrail name.
"""
from litellm.proxy.proxy_server import llm_router
if llm_router is None or not hasattr(llm_router, "guardrail_list"):
return False
matching = [
g
for g in llm_router.guardrail_list
if g.get("guardrail_name") == guardrail_name
]
return len(matching) > 1
async def _execute_guardrail_hook(
self,
callback: "CustomGuardrail",
hook_type: str,
data: dict,
user_api_key_dict: Optional[UserAPIKeyAuth],
call_type: CallTypesLiteral,
response: Optional[Any] = None,
) -> Any:
"""
Execute a single guardrail's hook.
Args:
callback: The guardrail callback to execute
hook_type: One of "pre_call", "during_call", "post_call"
data: Request data
user_api_key_dict: User API key auth
call_type: Type of call
response: Response object (for post_call hooks)
Returns:
Result from the guardrail execution
"""
# Use unified_guardrail if callback has apply_guardrail method
use_unified = "apply_guardrail" in type(callback).__dict__
if use_unified:
data["guardrail_to_apply"] = callback
target = unified_guardrail if use_unified else callback
if hook_type == "pre_call":
return await target.async_pre_call_hook(
user_api_key_dict=user_api_key_dict, # type: ignore
cache=self.call_details["user_api_key_cache"],
data=data,
call_type=call_type,
)
elif hook_type == "during_call":
return await target.async_moderation_hook(
data=data,
user_api_key_dict=user_api_key_dict, # type: ignore
call_type=call_type,
)
elif hook_type == "post_call":
return await target.async_post_call_success_hook(
user_api_key_dict=user_api_key_dict, # type: ignore
data=data,
response=response, # type: ignore
)
else:
raise ValueError(f"Unknown hook_type: {hook_type}")
async def _execute_guardrail_with_load_balancing(
self,
guardrail_name: str,
hook_type: str,
data: dict,
user_api_key_dict: Optional[UserAPIKeyAuth],
call_type: CallTypesLiteral,
response: Optional[Any] = None,
) -> Any:
"""
Execute a guardrail using the router's load balancing.
Args:
guardrail_name: Name of the guardrail
hook_type: One of "pre_call", "during_call", "post_call"
data: Request data
user_api_key_dict: User API key auth
call_type: Type of call
response: Response object (for post_call hooks)
Returns:
Result from the guardrail execution
"""
from litellm.proxy.proxy_server import llm_router
if llm_router is None:
raise ValueError("Router not initialized")
# Select guardrail using router's load balancing
selected_guardrail = llm_router.get_available_guardrail(
guardrail_name=guardrail_name
)
callback = selected_guardrail.get("callback")
if callback is None:
raise ValueError(f"No callback found for guardrail: {guardrail_name}")
return await self._execute_guardrail_hook(
callback=callback,
hook_type=hook_type,
data=data,
user_api_key_dict=user_api_key_dict,
call_type=call_type,
response=response,
)
async def _process_guardrail_callback(
self,
callback: CustomGuardrail,
@ -823,6 +950,8 @@ class ProxyLogging:
"""
Process a guardrail callback during pre-call hook.
Supports load balancing when multiple guardrail deployments exist.
Args:
callback: The CustomGuardrail callback to process
data: The request data dictionary
@ -843,23 +972,25 @@ class ProxyLogging:
if callback.should_run_guardrail(data=data, event_type=event_type) is not True:
return None
# Execute the appropriate guardrail hook
if "apply_guardrail" in type(callback).__dict__:
# Use unified guardrail for callbacks with apply_guardrail method
data["guardrail_to_apply"] = callback
response = await unified_guardrail.async_pre_call_hook(
user_api_key_dict=user_api_key_dict, # type: ignore
cache=self.call_details["user_api_key_cache"],
data=data, # type: ignore
call_type=call_type, # type: ignore
guardrail_name = callback.guardrail_name
# Check if load balancing should be used
if guardrail_name and self._should_use_guardrail_load_balancing(guardrail_name):
response = await self._execute_guardrail_with_load_balancing(
guardrail_name=guardrail_name,
hook_type="pre_call",
data=data,
user_api_key_dict=user_api_key_dict,
call_type=call_type,
)
else:
# Use the callback's own async_pre_call_hook method
response = await callback.async_pre_call_hook(
user_api_key_dict=user_api_key_dict, # type: ignore
cache=self.call_details["user_api_key_cache"],
data=data, # type: ignore
call_type=call_type, # type: ignore
# Single guardrail - execute directly
response = await self._execute_guardrail_hook(
callback=callback,
hook_type="pre_call",
data=data,
user_api_key_dict=user_api_key_dict,
call_type=call_type,
)
# Process the response if one was returned
@ -3583,7 +3714,10 @@ async def _monitor_spend_logs_queue(
db_writer_client: Optional HTTP handler for external spend logs endpoint
proxy_logging_obj: Proxy logging object
"""
from litellm.constants import SPEND_LOG_QUEUE_SIZE_THRESHOLD, SPEND_LOG_QUEUE_POLL_INTERVAL
from litellm.constants import (
SPEND_LOG_QUEUE_POLL_INTERVAL,
SPEND_LOG_QUEUE_SIZE_THRESHOLD,
)
threshold = SPEND_LOG_QUEUE_SIZE_THRESHOLD
base_interval = SPEND_LOG_QUEUE_POLL_INTERVAL

View File

@ -136,6 +136,7 @@ from litellm.types.router import (
CustomRoutingStrategyBase,
Deployment,
DeploymentTypedDict,
GuardrailTypedDict,
LiteLLM_Params,
MockRouterTestingParams,
ModelGroupInfo,
@ -214,6 +215,8 @@ class Router:
assistants_config: Optional[AssistantsTypedDict] = None,
## SEARCH API ##
search_tools: Optional[List[SearchToolTypedDict]] = None,
## GUARDRAIL API ##
guardrail_list: Optional[List[GuardrailTypedDict]] = None,
## CACHING ##
redis_url: Optional[str] = None,
redis_host: Optional[str] = None,
@ -375,6 +378,7 @@ class Router:
self.assistants_config = assistants_config
self.search_tools = search_tools or []
self.guardrail_list = guardrail_list or []
self.deployment_names: List = (
[]
) # names of models under litellm_params. ex. azure/chatgpt-v-2
@ -2974,6 +2978,99 @@ class Router:
**kwargs,
)
async def aguardrail(
self,
guardrail_name: str,
original_function: Callable,
**kwargs,
):
"""
Execute a guardrail with load balancing and fallbacks.
Args:
guardrail_name: Name of the guardrail to execute
original_function: The guardrail's execution function (e.g., async_pre_call_hook)
**kwargs: Additional arguments passed to the guardrail
Returns:
Result from the guardrail execution
"""
kwargs["model"] = guardrail_name # For fallback system compatibility
kwargs["original_generic_function"] = original_function
kwargs["original_function"] = self._aguardrail_helper
self._update_kwargs_before_fallbacks(
model=guardrail_name, kwargs=kwargs, metadata_variable_name="litellm_metadata"
)
verbose_router_logger.debug(
f"Inside aguardrail() - guardrail_name: {guardrail_name}; kwargs: {kwargs}"
)
response = await self.async_function_with_fallbacks(**kwargs)
return response
async def _aguardrail_helper(
self,
model: str,
original_generic_function: Callable,
**kwargs,
):
"""
Helper for aguardrail - selects a guardrail deployment and executes it.
Called by async_function_with_fallbacks for each retry attempt.
Args:
model: The guardrail_name (named 'model' for fallback system compatibility)
original_generic_function: The guardrail's execution function
**kwargs: Additional arguments
"""
guardrail_name = model
selected_guardrail = self.get_available_guardrail(
guardrail_name=guardrail_name,
)
verbose_router_logger.debug(
f"Selected guardrail deployment: {selected_guardrail.get('litellm_params', {}).get('guardrail')}"
)
# Pass the selected guardrail config to the original function
kwargs["selected_guardrail"] = selected_guardrail
response = await original_generic_function(**kwargs)
return response
def get_available_guardrail(
self,
guardrail_name: str,
) -> "GuardrailTypedDict":
"""
Select a guardrail deployment using the router's load balancing strategy.
Args:
guardrail_name: Name of the guardrail to select
Returns:
Selected guardrail configuration dict
"""
from litellm.router_strategy.simple_shuffle import simple_shuffle
healthy_deployments = [
g for g in self.guardrail_list if g.get("guardrail_name") == guardrail_name
]
if not healthy_deployments:
raise ValueError(f"No guardrail found with name: {guardrail_name}")
if len(healthy_deployments) == 1:
return healthy_deployments[0]
# Use simple_shuffle for weighted selection
return cast(
GuardrailTypedDict,
simple_shuffle(
llm_router_instance=self,
healthy_deployments=healthy_deployments,
model=guardrail_name,
),
)
async def _ageneric_api_call_with_fallbacks(
self, model: str, original_function: Callable, **kwargs
):

View File

@ -637,6 +637,29 @@ class SearchToolTypedDict(TypedDict):
litellm_params: Required[SearchToolLiteLLMParams]
class GuardrailLiteLLMParams(TypedDict, total=False):
"""
LiteLLM params for guardrails.
"""
guardrail: Required[str]
mode: Required[str]
api_key: Optional[str]
api_base: Optional[str]
weight: Optional[int] # For load balancing
class GuardrailTypedDict(TypedDict, total=False):
"""
Configuration for a guardrail in the router.
"""
guardrail_name: Required[str]
litellm_params: Required[GuardrailLiteLLMParams]
callback: Any # The CustomGuardrail instance
id: Optional[str] # Unique identifier for the guardrail deployment
class FineTuningConfig(BaseModel):
custom_llm_provider: Literal["azure", "openai"]

View File

@ -0,0 +1,105 @@
"""
Test guardrail load balancing through the Router and ProxyLogging.
"""
import os
import sys
from unittest.mock import MagicMock, patch, AsyncMock
sys.path.insert(0, os.path.abspath("../.."))
import litellm
import pytest
from litellm import Router
from litellm.caching import DualCache
from litellm.integrations.custom_guardrail import CustomGuardrail
from litellm.proxy.utils import ProxyLogging
from litellm.types.guardrails import GuardrailEventHooks
class MockGuardrail(CustomGuardrail):
"""Mock guardrail that tracks calls."""
call_count = 0
def __init__(self, guardrail_name: str, guardrail_id: str):
super().__init__(guardrail_name=guardrail_name)
self.guardrail_id = guardrail_id
self.calls = 0
def should_run_guardrail(self, data, event_type) -> bool:
return True
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type):
self.calls += 1
MockGuardrail.call_count += 1
return None
@pytest.mark.asyncio
async def test_proxy_logging_pre_call_hook_load_balancing():
"""Test that async_pre_call_hook load balances across multiple guardrails."""
# Reset call count
MockGuardrail.call_count = 0
# Create two mock guardrails with same name
guardrail_1 = MockGuardrail(guardrail_name="content-filter", guardrail_id="g1")
guardrail_2 = MockGuardrail(guardrail_name="content-filter", guardrail_id="g2")
# Create router with multiple guardrails of same name
guardrail_list = [
{
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"callback": guardrail_1,
"id": "guardrail-1",
},
{
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"callback": guardrail_2,
"id": "guardrail-2",
},
]
router = Router(
model_list=[
{
"model_name": "gpt-4",
"litellm_params": {"model": "gpt-4", "api_key": "fake-key"},
}
],
guardrail_list=guardrail_list,
)
# Create ProxyLogging instance
proxy_logging = ProxyLogging(user_api_key_cache=DualCache())
# Add guardrail to litellm.callbacks so it gets picked up
original_callbacks = litellm.callbacks.copy()
litellm.callbacks = [guardrail_1]
try:
with patch("litellm.proxy.proxy_server.llm_router", router):
# Call pre_call_hook 50 times
for _ in range(50):
await proxy_logging.pre_call_hook(
user_api_key_dict=MagicMock(),
data={"messages": [{"role": "user", "content": "test"}]},
call_type="completion",
)
# Both guardrails should have been called (load balanced)
assert guardrail_1.calls > 0, "Guardrail 1 should have been called"
assert guardrail_2.calls > 0, "Guardrail 2 should have been called"
# Total calls should be 50
total = guardrail_1.calls + guardrail_2.calls
assert total == 50, f"Expected 50 total calls, got {total}"
# Verify reasonable distribution (not all to one)
min_calls = min(guardrail_1.calls, guardrail_2.calls)
assert min_calls >= 10, f"Expected at least 10 calls to each guardrail, got min={min_calls}"
finally:
litellm.callbacks = original_callbacks

View File

@ -315,3 +315,46 @@ async def test_guardrails_with_team_controls():
assert "x-litellm-applied-guardrails" in headers
assert headers["x-litellm-applied-guardrails"] == "bedrock-pre-guard"
async def get_guardrail_lb_counts(session):
"""Get the current guardrail load balancing call counts from the proxy."""
url = "http://0.0.0.0:4000/guardrail/lb/counts"
headers = {"Authorization": "Bearer sk-1234", "Content-Type": "application/json"}
async with session.get(url, headers=headers) as response:
if response.status == 200:
return await response.json()
return None
@pytest.mark.asyncio
async def test_guardrail_load_balancing():
"""
Test that guardrail load balancing distributes requests across multiple guardrail instances.
- Make 20 requests with the lb-test-guard guardrail
- Verify that both GuardrailForLBTestingA and GuardrailForLBTestingB are called
- Verify reasonable distribution (both should have at least some calls)
"""
async with aiohttp.ClientSession() as session:
num_requests = 20
# Make multiple requests with the load-balanced guardrail
for i in range(num_requests):
response, headers = await chat_completion(
session,
"sk-1234",
model="fake-openai-endpoint",
messages=[{"role": "user", "content": f"Hello request {i}"}],
guardrails=["lb-test-guard"],
)
# Verify guardrail was applied
assert "x-litellm-applied-guardrails" in headers
assert headers["x-litellm-applied-guardrails"] == "lb-test-guard"
# All requests should succeed - the test passes if we get here
# The actual load balancing verification is done by checking proxy logs
# which should show alternating calls to GuardrailForLBTestingA and GuardrailForLBTestingB
print(f"Successfully made {num_requests} requests with load-balanced guardrail")

View File

@ -1724,3 +1724,148 @@ def test_get_deployment_credentials_with_provider_aws_bedrock_runtime_endpoint()
assert credentials["aws_secret_access_key"] == "test-secret-key"
assert credentials["aws_region_name"] == "us-east-1"
assert credentials["custom_llm_provider"] == "bedrock"
def test_get_available_guardrail_single_deployment():
"""
Test get_available_guardrail returns the single guardrail when only one exists.
"""
guardrail_config = {
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"id": "guardrail-1",
}
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
}
],
guardrail_list=[guardrail_config],
)
result = router.get_available_guardrail(guardrail_name="content-filter")
assert result == guardrail_config
def test_get_available_guardrail_multiple_deployments():
"""
Test get_available_guardrail load balances across multiple guardrails.
"""
guardrail_1 = {
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"id": "guardrail-1",
}
guardrail_2 = {
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"id": "guardrail-2",
}
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
}
],
guardrail_list=[guardrail_1, guardrail_2],
)
# Call multiple times to verify load balancing
results = set()
for _ in range(20):
result = router.get_available_guardrail(guardrail_name="content-filter")
results.add(result["id"])
# Both guardrails should be selected at least once
assert "guardrail-1" in results or "guardrail-2" in results
def test_get_available_guardrail_not_found():
"""
Test get_available_guardrail raises ValueError when guardrail not found.
"""
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
}
],
guardrail_list=[],
)
with pytest.raises(ValueError, match="No guardrail found with name"):
router.get_available_guardrail(guardrail_name="non-existent")
@pytest.mark.asyncio
async def test_aguardrail_helper():
"""
Test _aguardrail_helper selects a guardrail and executes the original function.
"""
guardrail_config = {
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"id": "guardrail-1",
}
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
}
],
guardrail_list=[guardrail_config],
)
# Mock the original function
async def mock_original_function(**kwargs):
return {"result": "success", "selected_guardrail": kwargs.get("selected_guardrail")}
result = await router._aguardrail_helper(
model="content-filter",
original_generic_function=mock_original_function,
)
assert result["result"] == "success"
assert result["selected_guardrail"] == guardrail_config
@pytest.mark.asyncio
async def test_aguardrail():
"""
Test aguardrail executes a guardrail with load balancing and fallbacks.
"""
guardrail_config = {
"guardrail_name": "content-filter",
"litellm_params": {"guardrail": "custom", "mode": "pre_call"},
"id": "guardrail-1",
}
router = litellm.Router(
model_list=[
{
"model_name": "gpt-3.5-turbo",
"litellm_params": {"model": "gpt-3.5-turbo"},
}
],
guardrail_list=[guardrail_config],
)
# Mock the original function
async def mock_original_function(**kwargs):
return {"result": "success", "selected_guardrail": kwargs.get("selected_guardrail")}
result = await router.aguardrail(
guardrail_name="content-filter",
original_function=mock_original_function,
)
assert result["result"] == "success"
assert result["selected_guardrail"]["id"] == "guardrail-1"