This commit is contained in:
parent
c94f61b1da
commit
28821427ce
@ -413,6 +413,12 @@ router_settings:
|
||||
| AZURE_FEDERATED_TOKEN_FILE | File path to Azure federated token
|
||||
| AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY | Cost per GB per day for Azure File Search service
|
||||
| AZURE_SCOPE | For EntraID Auth, Scope for Azure services, defaults to "https://cognitiveservices.azure.com/.default"
|
||||
| AZURE_SENTINEL_DCR_IMMUTABLE_ID | Immutable ID of the Data Collection Rule for Azure Sentinel logging
|
||||
| AZURE_SENTINEL_STREAM_NAME | Stream name for Azure Sentinel logging
|
||||
| AZURE_SENTINEL_CLIENT_SECRET | Client secret for Azure Sentinel authentication
|
||||
| AZURE_SENTINEL_ENDPOINT | Endpoint for Azure Sentinel logging
|
||||
| AZURE_SENTINEL_TENANT_ID | Tenant ID for Azure Sentinel authentication
|
||||
| AZURE_SENTINEL_CLIENT_ID | Client ID for Azure Sentinel authentication
|
||||
| AZURE_KEY_VAULT_URI | URI for Azure Key Vault
|
||||
| AZURE_OPERATION_POLLING_TIMEOUT | Timeout in seconds for Azure operation polling
|
||||
| AZURE_STORAGE_ACCOUNT_KEY | The Azure Storage Account Key to use for Authentication to Azure Blob Storage logging
|
||||
@ -541,6 +547,8 @@ router_settings:
|
||||
| DOCS_TITLE | Title of the documentation pages
|
||||
| DOCS_URL | The path to the Swagger API documentation. **By default this is "/"**
|
||||
| EMAIL_LOGO_URL | URL for the logo used in emails
|
||||
| EMAIL_BUDGET_ALERT_TTL | Time-to-live for email budget alerts in seconds
|
||||
| EMAIL_BUDGET_ALERT_MAX_SPEND_ALERT_PERCENTAGE | Maximum spend percentage for triggering email budget alerts
|
||||
| EMAIL_SUPPORT_CONTACT | Support contact email address
|
||||
| EMAIL_SIGNATURE | Custom HTML footer/signature for all emails. Can include HTML tags for formatting and links.
|
||||
| EMAIL_SUBJECT_INVITATION | Custom subject template for invitation emails.
|
||||
@ -596,6 +604,8 @@ router_settings:
|
||||
| GREENSCALE_ENDPOINT | Endpoint URL for Greenscale service
|
||||
| GRAYSWAN_API_BASE | Base URL for GraySwan API. Default is https://api.grayswan.ai
|
||||
| GRAYSWAN_API_KEY | API key for GraySwan Cygnal service
|
||||
| GRAYSWAN_REASONING_MODE | Reasoning mode for GraySwan guardrail
|
||||
| GRAYSWAN_VIOLATION_THRESHOLD | Violation threshold for GraySwan guardrail
|
||||
| GOOGLE_APPLICATION_CREDENTIALS | Path to Google Cloud credentials JSON file
|
||||
| GOOGLE_CLIENT_ID | Client ID for Google OAuth
|
||||
| GOOGLE_CLIENT_SECRET | Client secret for Google OAuth
|
||||
@ -825,6 +835,7 @@ router_settings:
|
||||
| SMTP_TLS | Flag to enable or disable TLS for SMTP connections
|
||||
| SMTP_USERNAME | Username for SMTP authentication (do not set if SMTP does not require auth)
|
||||
| SENDGRID_API_KEY | API key for SendGrid email service
|
||||
| RESEND_API_KEY | API key for Resend email service
|
||||
| SENDGRID_SENDER_EMAIL | Email address used as the sender in SendGrid email transactions
|
||||
| SPEND_LOGS_URL | URL for retrieving spend logs
|
||||
| SPEND_LOG_CLEANUP_BATCH_SIZE | Number of logs deleted per batch during cleanup. Default is 1000
|
||||
|
||||
@ -692,12 +692,15 @@ class ModelResponseIterator:
|
||||
text = content_block_start["content_block"]["text"]
|
||||
elif content_block_start["content_block"]["type"] == "tool_use" or content_block_start["content_block"]["type"] == "server_tool_use":
|
||||
self.tool_index += 1
|
||||
# Some server_tool_use blocks (e.g. web_search) may omit `input` at start;
|
||||
# default to {} to avoid KeyError and let deltas populate arguments.
|
||||
tool_input = content_block_start["content_block"].get("input", {})
|
||||
tool_use = ChatCompletionToolCallChunk(
|
||||
id=content_block_start["content_block"]["id"],
|
||||
type="function",
|
||||
function=ChatCompletionToolCallFunctionChunk(
|
||||
name=content_block_start["content_block"]["name"],
|
||||
arguments=str(content_block_start["content_block"]["input"]),
|
||||
arguments=str(tool_input),
|
||||
),
|
||||
index=self.tool_index,
|
||||
)
|
||||
|
||||
@ -21,13 +21,7 @@ from litellm._logging import verbose_proxy_logger
|
||||
from litellm.llms.base_llm.guardrail_translation.base_translation import BaseTranslation
|
||||
from litellm.main import stream_chunk_builder
|
||||
from litellm.types.llms.openai import ChatCompletionToolParam
|
||||
from litellm.types.utils import (
|
||||
Choices,
|
||||
GenericGuardrailAPIInputs,
|
||||
ModelResponse,
|
||||
ModelResponseStream,
|
||||
StreamingChoices,
|
||||
)
|
||||
from litellm.types.utils import Choices, GenericGuardrailAPIInputs, ModelResponse, ModelResponseStream, StreamingChoices
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
|
||||
@ -31097,7 +31097,8 @@
|
||||
"input_cost_per_token": 2e-07,
|
||||
"output_cost_per_token": 2e-07,
|
||||
"litellm_provider": "fireworks_ai",
|
||||
"mode": "chat"
|
||||
"mode": "chat",
|
||||
"supports_reasoning": true
|
||||
},
|
||||
"fireworks_ai/accounts/fireworks/models/qwen3-coder-30b-a3b-instruct": {
|
||||
"max_tokens": 262144,
|
||||
|
||||
@ -1461,6 +1461,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase):
|
||||
team_member_key_duration: Optional[str] = None
|
||||
allowed_passthrough_routes: Optional[list] = None
|
||||
secret_manager_settings: Optional[dict] = None
|
||||
prompts: Optional[List[str]] = None
|
||||
model_rpm_limit: Optional[Dict[str, int]] = None
|
||||
model_tpm_limit: Optional[Dict[str, int]] = None
|
||||
allowed_vector_store_indexes: Optional[List[AllowedVectorStoreIndexItem]] = None
|
||||
|
||||
@ -31,7 +31,6 @@ from litellm.integrations.custom_guardrail import CustomGuardrail
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.types.utils import GenericGuardrailAPIInputs
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.types.guardrails import (
|
||||
BlockedWord,
|
||||
ContentFilterAction,
|
||||
@ -42,8 +41,6 @@ from litellm.types.guardrails import (
|
||||
from litellm.types.proxy.guardrails.guardrail_hooks.litellm_content_filter import (
|
||||
ContentFilterCategoryConfig,
|
||||
)
|
||||
from litellm.types.utils import ModelResponseStream
|
||||
|
||||
from .patterns import get_compiled_pattern
|
||||
|
||||
|
||||
@ -243,16 +240,16 @@ class ContentFilterGuardrail(CustomGuardrail):
|
||||
continue
|
||||
|
||||
try:
|
||||
category = self._load_category_file(category_file_path)
|
||||
self.loaded_categories[category_name] = category
|
||||
category_config_obj = self._load_category_file(category_file_path)
|
||||
self.loaded_categories[category_name] = category_config_obj
|
||||
|
||||
# Use action from config, or default from category file
|
||||
category_action = ContentFilterAction(
|
||||
action if action else category.default_action
|
||||
action if action else category_config_obj.default_action
|
||||
)
|
||||
|
||||
# Add keywords from this category
|
||||
for keyword_data in category.keywords:
|
||||
for keyword_data in category_config_obj.keywords:
|
||||
keyword = keyword_data["keyword"].lower()
|
||||
severity = keyword_data["severity"]
|
||||
|
||||
@ -266,7 +263,7 @@ class ContentFilterGuardrail(CustomGuardrail):
|
||||
|
||||
verbose_proxy_logger.info(
|
||||
f"Loaded category {category_name}: "
|
||||
f"{len(category.keywords)} keywords"
|
||||
f"{len(category_config_obj.keywords)} keywords"
|
||||
)
|
||||
except Exception as e:
|
||||
verbose_proxy_logger.error(
|
||||
@ -534,10 +531,10 @@ class ContentFilterGuardrail(CustomGuardrail):
|
||||
# Check category keywords
|
||||
category_keyword_match = self._check_category_keywords(text, all_exceptions)
|
||||
if category_keyword_match:
|
||||
keyword, category, severity, action = category_keyword_match
|
||||
keyword, category_name, severity, action = category_keyword_match
|
||||
if action == ContentFilterAction.BLOCK:
|
||||
error_msg = (
|
||||
f"Content blocked: {category} category keyword '{keyword}' detected "
|
||||
f"Content blocked: {category_name} category keyword '{keyword}' detected "
|
||||
f"(severity: {severity})"
|
||||
)
|
||||
verbose_proxy_logger.warning(error_msg)
|
||||
@ -545,7 +542,7 @@ class ContentFilterGuardrail(CustomGuardrail):
|
||||
status_code=403,
|
||||
detail={
|
||||
"error": error_msg,
|
||||
"category": category,
|
||||
"category": category_name,
|
||||
"keyword": keyword,
|
||||
"severity": severity,
|
||||
},
|
||||
@ -559,7 +556,7 @@ class ContentFilterGuardrail(CustomGuardrail):
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
verbose_proxy_logger.info(
|
||||
f"Masked category keyword '{keyword}' from {category} (severity: {severity})"
|
||||
f"Masked category keyword '{keyword}' from {category_name} (severity: {severity})"
|
||||
)
|
||||
|
||||
# Check regex patterns - process ALL patterns, not just first match
|
||||
@ -690,8 +687,10 @@ class ContentFilterGuardrail(CustomGuardrail):
|
||||
responses = await asyncio.gather(*tasks)
|
||||
descriptions = []
|
||||
for response in responses:
|
||||
if response.choices[0].message.content:
|
||||
image_description = response.choices[0].message.content
|
||||
choice = response.choices[0]
|
||||
message = getattr(choice, "message", None)
|
||||
if message and getattr(message, "content", None):
|
||||
image_description = message.content
|
||||
verbose_proxy_logger.debug(
|
||||
f"Image description: {image_description}"
|
||||
)
|
||||
|
||||
@ -37,39 +37,72 @@ from litellm.secret_managers.main import get_secret
|
||||
|
||||
def _resolve_os_environ_variables(params: dict) -> dict:
|
||||
"""
|
||||
Resolve os.environ/ environment variables in litellm_params.
|
||||
|
||||
This function recursively processes dictionary values that start with "os.environ/"
|
||||
by replacing them with the actual environment variable values.
|
||||
|
||||
Args:
|
||||
params: Dictionary containing litellm_params that may have os.environ/ values
|
||||
|
||||
Returns:
|
||||
Dictionary with os.environ/ values resolved to actual environment variable values
|
||||
Resolve ``os.environ/`` environment variables in ``litellm_params``.
|
||||
|
||||
This walks the input dict/list structure iteratively (no Python recursion) to
|
||||
avoid unbounded recursion / stack overflows on deeply nested inputs.
|
||||
"""
|
||||
if not isinstance(params, dict):
|
||||
return params
|
||||
|
||||
resolved_params = {}
|
||||
for key, value in params.items():
|
||||
if isinstance(value, str) and value.startswith("os.environ/"):
|
||||
# Resolve the environment variable
|
||||
resolved_value = get_secret(value)
|
||||
resolved_params[key] = resolved_value
|
||||
elif isinstance(value, dict):
|
||||
# Recursively resolve nested dictionaries
|
||||
resolved_params[key] = _resolve_os_environ_variables(value)
|
||||
elif isinstance(value, list):
|
||||
# Handle lists that might contain dictionaries with os.environ/ values
|
||||
resolved_params[key] = [
|
||||
_resolve_os_environ_variables(item) if isinstance(item, dict) else item
|
||||
for item in value
|
||||
]
|
||||
else:
|
||||
resolved_params[key] = value
|
||||
|
||||
return resolved_params
|
||||
|
||||
# Use an explicit stack to avoid recursion and handle nested dicts/lists.
|
||||
# We also keep a `seen` set to guard against accidental cycles.
|
||||
resolved_root: dict = {}
|
||||
stack: list[tuple[object, object]] = [(params, resolved_root)]
|
||||
seen: set[int] = {id(params)}
|
||||
|
||||
while stack:
|
||||
src, dst = stack.pop()
|
||||
|
||||
if isinstance(src, dict) and isinstance(dst, dict):
|
||||
for key, value in src.items():
|
||||
# Direct string replacement for os.environ/ references
|
||||
if isinstance(value, str) and value.startswith("os.environ/"):
|
||||
dst[key] = get_secret(value)
|
||||
elif isinstance(value, dict):
|
||||
if id(value) in seen:
|
||||
# Cycle detected – keep a shallow copy reference to prevent infinite loops
|
||||
dst[key] = {}
|
||||
continue
|
||||
seen.add(id(value))
|
||||
new_dict: dict = {}
|
||||
dst[key] = new_dict
|
||||
stack.append((value, new_dict))
|
||||
elif isinstance(value, list):
|
||||
if id(value) in seen:
|
||||
dst[key] = []
|
||||
continue
|
||||
seen.add(id(value))
|
||||
new_list: list = []
|
||||
dst[key] = new_list
|
||||
stack.append((value, new_list))
|
||||
else:
|
||||
dst[key] = value
|
||||
|
||||
elif isinstance(src, list) and isinstance(dst, list):
|
||||
for item in src:
|
||||
if isinstance(item, str) and item.startswith("os.environ/"):
|
||||
dst.append(get_secret(item))
|
||||
elif isinstance(item, dict):
|
||||
if id(item) in seen:
|
||||
dst.append({})
|
||||
continue
|
||||
seen.add(id(item))
|
||||
new_dict = {}
|
||||
dst.append(new_dict)
|
||||
stack.append((item, new_dict))
|
||||
elif isinstance(item, list):
|
||||
if id(item) in seen:
|
||||
dst.append([])
|
||||
continue
|
||||
seen.add(id(item))
|
||||
new_list = []
|
||||
dst.append(new_list)
|
||||
stack.append((item, new_list))
|
||||
else:
|
||||
dst.append(item)
|
||||
|
||||
return resolved_root
|
||||
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -678,15 +678,14 @@ async def new_team( # noqa: PLR0915
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- prompts: Optional[List[str]] - List of prompts that the team is allowed to use.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"], "agents": ["agent_1", "agent_2"], "agent_access_groups": ["dev_group"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
- team_member_rpm_limit: Optional[int] - The RPM (Requests Per Minute) limit for individual team members.
|
||||
- team_member_tpm_limit: Optional[int] - The TPM (Tokens Per Minute) limit for individual team members.
|
||||
- team_member_key_duration: Optional[str] - The duration for a team member's key. e.g. "1d", "1w", "1mo"
|
||||
- prompts: Optional[List[str]] - List of allowed prompts for the team. If specified, the team will only be able to use these specific prompts.
|
||||
- allowed_passthrough_routes: Optional[List[str]] - List of allowed pass through routes for the team.
|
||||
- allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint.
|
||||
- secret_manager_settings: Optional[dict] - Secret manager settings for the team. [Docs](https://docs.litellm.ai/docs/secret_managers/overview)
|
||||
|
||||
|
||||
|
||||
@ -1201,7 +1200,6 @@ async def update_team(
|
||||
- model_aliases: Optional[dict] - Model aliases for the team. [Docs](https://docs.litellm.ai/docs/proxy/team_based_routing#create-team-with-model-alias)
|
||||
- guardrails: Optional[List[str]] - Guardrails for the team. [Docs](https://docs.litellm.ai/docs/proxy/guardrails)
|
||||
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
|
||||
- prompts: Optional[List[str]] - List of prompts that the team is allowed to use.
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - team-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"], "agents": ["agent_1", "agent_2"], "agent_access_groups": ["dev_group"]}. IF null or {} then no object permission.
|
||||
- team_member_budget: Optional[float] - The maximum budget allocated to an individual team member.
|
||||
- team_member_rpm_limit: Optional[int] - The RPM (Requests Per Minute) limit for individual team members.
|
||||
@ -1212,6 +1210,7 @@ async def update_team(
|
||||
- model_tpm_limit: Optional[Dict[str, int]] - The TPM (Tokens Per Minute) limit per model for this team. Example: {"gpt-4": 10000, "gpt-3.5-turbo": 20000}
|
||||
Example - update team TPM Limit
|
||||
- allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint.
|
||||
- secret_manager_settings: Optional[dict] - Secret manager settings for the team. [Docs](https://docs.litellm.ai/docs/secret_managers/overview)
|
||||
|
||||
|
||||
```
|
||||
|
||||
@ -31103,7 +31103,8 @@
|
||||
"input_cost_per_token": 2e-07,
|
||||
"output_cost_per_token": 2e-07,
|
||||
"litellm_provider": "fireworks_ai",
|
||||
"mode": "chat"
|
||||
"mode": "chat",
|
||||
"supports_reasoning": true
|
||||
},
|
||||
"fireworks_ai/accounts/fireworks/models/qwen3-coder-30b-a3b-instruct": {
|
||||
"max_tokens": 262144,
|
||||
|
||||
@ -14,6 +14,7 @@ SEARCH_PROVIDERS = [
|
||||
"exa_ai",
|
||||
"firecrawl",
|
||||
"searxng",
|
||||
"linkup",
|
||||
]
|
||||
|
||||
ALLOWED_FILES_IN_LLMS_FOLDER = [
|
||||
|
||||
@ -10,7 +10,7 @@ sys.path.insert(0, os.path.abspath("../.."))
|
||||
from litellm.proxy.guardrails.guardrail_hooks.dynamoai import DynamoAIGuardrails
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.caching.caching import DualCache
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@ -48,26 +48,25 @@ async def test_dynamoai_blocks_content_with_block_action():
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
guardrail.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
with patch.object(guardrail.async_handler, "post", AsyncMock(return_value=mock_response)):
|
||||
request_data = {
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "This is harmful content"}
|
||||
],
|
||||
}
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "This is harmful content"}
|
||||
],
|
||||
}
|
||||
# Mock should_run_guardrail to return True
|
||||
guardrail.should_run_guardrail = MagicMock(return_value=True)
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
guardrail.should_run_guardrail = MagicMock(return_value=True)
|
||||
|
||||
# Test that the guardrail raises ValueError for blocked content
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await guardrail.async_pre_call_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
cache=MagicMock(spec=DualCache),
|
||||
)
|
||||
# Test that the guardrail raises ValueError for blocked content
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
await guardrail.async_pre_call_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
cache=MagicMock(spec=DualCache),
|
||||
)
|
||||
|
||||
# Verify the error message contains policy information
|
||||
error_message = str(exc_info.value)
|
||||
@ -98,25 +97,24 @@ async def test_dynamoai_allows_content_with_none_action():
|
||||
"appliedPolicies": []
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
guardrail.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
with patch.object(guardrail.async_handler, "post", AsyncMock(return_value=mock_response)):
|
||||
request_data = {
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
}
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
],
|
||||
}
|
||||
# Mock should_run_guardrail to return True
|
||||
guardrail.should_run_guardrail = MagicMock(return_value=True)
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
guardrail.should_run_guardrail = MagicMock(return_value=True)
|
||||
|
||||
# Test that the guardrail allows the content (no exception raised)
|
||||
result = await guardrail.async_pre_call_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
cache=MagicMock(spec=DualCache),
|
||||
)
|
||||
# Test that the guardrail allows the content (no exception raised)
|
||||
result = await guardrail.async_pre_call_hook(
|
||||
data=request_data,
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
call_type="completion",
|
||||
cache=MagicMock(spec=DualCache),
|
||||
)
|
||||
|
||||
# Should return the request data unchanged
|
||||
assert result == request_data
|
||||
|
||||
@ -282,8 +282,6 @@ async def test_bedrock_guardrail_status_blocked():
|
||||
aws_region_name="us-east-1",
|
||||
)
|
||||
|
||||
# Mock Bedrock API response indicating content was blocked
|
||||
# action="GUARDRAIL_INTERVENED" means the guardrail blocked the request
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
@ -295,33 +293,32 @@ async def test_bedrock_guardrail_status_blocked():
|
||||
}
|
||||
}]
|
||||
}
|
||||
bedrock_guard.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "harmful content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to ensure guardrail logic executes
|
||||
with patch.object(bedrock_guard, 'should_run_guardrail', return_value=True):
|
||||
# Call guardrail pre_call hook - this will raise an exception when content is blocked
|
||||
try:
|
||||
await bedrock_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
except Exception:
|
||||
# Expected exception when guardrail blocks content
|
||||
pass
|
||||
|
||||
# Call litellm.acompletion to trigger logging callbacks
|
||||
# This populates the standard_logging_payload in our custom logger
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
with patch.object(bedrock_guard.async_handler, "post", AsyncMock(return_value=mock_response)):
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "harmful content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to ensure guardrail logic executes
|
||||
with patch.object(bedrock_guard, 'should_run_guardrail', return_value=True):
|
||||
# Call guardrail pre_call hook - this will raise an exception when content is blocked
|
||||
try:
|
||||
await bedrock_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
except Exception:
|
||||
# Expected exception when guardrail blocks content
|
||||
pass
|
||||
|
||||
# Call litellm.acompletion to trigger logging callbacks
|
||||
# This populates the standard_logging_payload in our custom logger
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Verify the standard logging payload was captured
|
||||
assert test_custom_logger.standard_logging_payload is not None
|
||||
@ -383,27 +380,26 @@ async def test_bedrock_guardrail_status_success():
|
||||
"outputs": [{"text": "Safe content"}],
|
||||
"assessments": []
|
||||
}
|
||||
bedrock_guard.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "safe content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(bedrock_guard, 'should_run_guardrail', return_value=True):
|
||||
await bedrock_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
with patch.object(bedrock_guard.async_handler, "post", AsyncMock(return_value=mock_response)):
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "safe content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(bedrock_guard, 'should_run_guardrail', return_value=True):
|
||||
await bedrock_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Check standard logging payload status fields
|
||||
assert test_custom_logger.standard_logging_payload is not None
|
||||
@ -456,34 +452,31 @@ async def test_bedrock_guardrail_status_failure():
|
||||
)
|
||||
|
||||
# Mock network failure (endpoint down)
|
||||
bedrock_guard.async_handler.post = AsyncMock(
|
||||
side_effect=httpx.ConnectError("Connection failed")
|
||||
)
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "test content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(bedrock_guard, 'should_run_guardrail', return_value=True):
|
||||
# Call guardrail (will raise exception on network failure)
|
||||
try:
|
||||
await bedrock_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
except Exception:
|
||||
# Expected exception when endpoint is down
|
||||
pass
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
with patch.object(bedrock_guard.async_handler, "post", AsyncMock(side_effect=httpx.ConnectError("Connection failed"))):
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "test content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(bedrock_guard, 'should_run_guardrail', return_value=True):
|
||||
# Call guardrail (will raise exception on network failure)
|
||||
try:
|
||||
await bedrock_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
except Exception:
|
||||
# Expected exception when endpoint is down
|
||||
pass
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Check standard logging payload status fields
|
||||
assert test_custom_logger.standard_logging_payload is not None
|
||||
@ -544,31 +537,30 @@ async def test_noma_guardrail_status_blocked():
|
||||
}
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
noma_guard.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "harmful content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(noma_guard, 'should_run_guardrail', return_value=True):
|
||||
# Call guardrail (will raise exception on block)
|
||||
try:
|
||||
await noma_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
with patch.object(noma_guard.async_handler, "post", AsyncMock(return_value=mock_response)):
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "harmful content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(noma_guard, 'should_run_guardrail', return_value=True):
|
||||
# Call guardrail (will raise exception on block)
|
||||
try:
|
||||
await noma_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Check standard logging payload status fields
|
||||
assert test_custom_logger.standard_logging_payload is not None
|
||||
@ -625,27 +617,26 @@ async def test_noma_guardrail_status_success():
|
||||
"originalResponse": {"prompt": {}}
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
noma_guard.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "safe content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(noma_guard, 'should_run_guardrail', return_value=True):
|
||||
await noma_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
with patch.object(noma_guard.async_handler, "post", AsyncMock(return_value=mock_response)):
|
||||
request_data = {
|
||||
"model": "gpt-4o",
|
||||
"messages": [{"role": "user", "content": "safe content"}],
|
||||
"mock_response": "Hello",
|
||||
"metadata": {}
|
||||
}
|
||||
|
||||
# Mock should_run_guardrail to return True
|
||||
with patch.object(noma_guard, 'should_run_guardrail', return_value=True):
|
||||
await noma_guard.async_pre_call_hook(
|
||||
user_api_key_dict=UserAPIKeyAuth(),
|
||||
cache=None,
|
||||
data=request_data,
|
||||
call_type="completion"
|
||||
)
|
||||
|
||||
# Call litellm.acompletion to trigger logging
|
||||
response = await litellm.acompletion(**request_data)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Check standard logging payload status fields
|
||||
assert test_custom_logger.standard_logging_payload is not None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -495,13 +495,13 @@ async def test_bedrock_guardrail_make_api_request_passes_api_key():
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"action": "NONE", "outputs": []}
|
||||
guardrail_hook.async_handler.post = AsyncMock(return_value=mock_response)
|
||||
|
||||
test_request_data = {
|
||||
"api_key": "test-api-key-789"
|
||||
}
|
||||
|
||||
with patch.object(guardrail_hook, "_load_credentials") as mock_load_creds, \
|
||||
with patch.object(guardrail_hook.async_handler, "post", AsyncMock(return_value=mock_response)), \
|
||||
patch.object(guardrail_hook, "_load_credentials") as mock_load_creds, \
|
||||
patch.object(guardrail_hook, "convert_to_bedrock_format") as mock_convert, \
|
||||
patch.object(guardrail_hook, "get_guardrail_dynamic_request_body_params") as mock_get_params, \
|
||||
patch.object(guardrail_hook, "add_standard_logging_guardrail_information_to_request_data"), \
|
||||
|
||||
Loading…
Reference in New Issue
Block a user