Configure full Bedrock guardrail assessments (#49217) (#49470)

This commit is contained in:
ishaan-j-berri 2026-10-31 16:37:45 -07:00 • committed by GitHub
parent 2751cfcad3
commit 6105aaa63e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 109 additions and 6 deletions

View file

@ -16484,6 +16484,14 @@
"title": "BedrockChecksSensitiveInformationModel",
"type": "object"
},
"BedrockGuardrailOutputScope": {
"enum": [
"INTERVENTIONS",
"FULL"
],
"title": "BedrockGuardrailOutputScope",
"type": "string"
},
"BlockedWord": {
"description": "Represents a blocked word with its action and optional description",
"properties": {
@ -18716,6 +18724,17 @@
],
"description": "Optional parameters for the guardrail"
},
"outputScope": {
"anyOf": [
{
"$ref": "#/components/schemas/BedrockGuardrailOutputScope"
},
{
"type": "null"
}
],
"description": "ApplyGuardrail response scope. FULL includes detected and non-detected findings. Omit to retain Bedrock's default of returning detected findings only."
},
"output_parse_pii": {
"anyOf": [
{

View file

@ -65,6 +65,7 @@ from litellm.proxy.guardrails.anthropic_sse import (
)
from litellm.types.guardrails import (
BedrockChecksConfigModel,
BedrockGuardrailOutputScope,
BedrockGuardrailStreamingParams,
GuardrailEventHooks,
LitellmParams,
@ -289,6 +290,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
streaming_buffer_release_on_scan: bool | None = None,
streaming_scan_in_background: bool | None = None,
async_handler: AsyncHTTPHandler | None = None,
outputScope: BedrockGuardrailOutputScope | None = None,
**kwargs,
):
self.async_handler = async_handler or get_async_httpx_client(
@ -309,6 +311,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
)
self.guardrailIdentifier = guardrailIdentifier
self.guardrailVersion = guardrailVersion
self.outputScope = outputScope
self.guardrail_provider = "bedrock"
self.chunk_budget_chars = chunk_budget_chars
self.contextual_grounding_from_messages = contextual_grounding_from_messages
@ -1280,10 +1283,10 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
AWS billed them to ``completed_chunk_usages``, and the attempt log sums those
with the blocking call's own usage.
"""
bedrock_request_data: Final = {
**base_request_data,
"content": content,
}
bedrock_request_data: Final = dict(base_request_data)
bedrock_request_data["content"] = content
if self.outputScope is not None and "outputScope" not in bedrock_request_data:
bedrock_request_data["outputScope"] = self.outputScope.value
prepared_request: Final = await run_aws_signing(
self._prepare_request,
credentials=credentials,

View file

@ -18,6 +18,7 @@ def initialize_bedrock(litellm_params: LitellmParams, guardrail: Guardrail):
event_hook=litellm_params.mode,
guardrailIdentifier=litellm_params.guardrailIdentifier,
guardrailVersion=litellm_params.guardrailVersion,
outputScope=litellm_params.outputScope,
checks=litellm_params.checks if isinstance(litellm_params.checks, BedrockChecksConfigModel) else None,
content_filter_threshold=litellm_params.content_filter_threshold,
prompt_attack_threshold=litellm_params.prompt_attack_threshold,

View file

@ -604,6 +604,11 @@ class BedrockChecksConfigModel(LiteLLMBaseModel):
return self
class BedrockGuardrailOutputScope(Enum):
INTERVENTIONS = "INTERVENTIONS"
FULL = "FULL"
class BedrockGuardrailConfigModel(LiteLLMBaseModel):
"""Configuration parameters for the AWS Bedrock guardrail"""
@ -612,6 +617,11 @@ class BedrockGuardrailConfigModel(LiteLLMBaseModel):
default=None,
description="The version of your Bedrock guardrail (e.g., DRAFT or version number)",
)
outputScope: BedrockGuardrailOutputScope | None = Field(
default=None,
description="ApplyGuardrail response scope. FULL includes detected and non-detected findings. "
"Omit to retain Bedrock's default of returning detected findings only.",
)
disable_exception_on_block: bool | None = Field(
default=False,
description="If True, will not raise an exception when the guardrail is blocked. Useful for OpenWebUI where exceptions can end the chat flow.",

View file

@ -27,8 +27,9 @@ from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import (
BedrockGuardrail,
_redact_pii_matches,
)
from litellm.proxy.guardrails.guardrail_initializers import initialize_bedrock
from litellm.proxy.utils import ProxyLogging
from litellm.types.guardrails import GuardrailEventHooks
from litellm.types.guardrails import BedrockGuardrailOutputScope, Guardrail, GuardrailEventHooks, LitellmParams
from litellm.types.proxy.guardrails.guardrail_hooks.bedrock_guardrails import (
BedrockContentItem,
BedrockTextContent,
@ -1020,6 +1021,64 @@ def _anthropic_tool_result_conversation(
]
@pytest.mark.asyncio
@pytest.mark.parametrize("scope", [None, "INTERVENTIONS", "FULL"])
async def test_configured_output_scope_reaches_bedrock_and_preserves_healthy_assessments(
monkeypatch: pytest.MonkeyPatch, respx_mock: respx.MockRouter, scope: str | None
) -> None:
monkeypatch.setattr(litellm, "disable_aiohttp_transport", True)
params: Final = LitellmParams.model_validate(
{
"guardrail": "bedrock",
"mode": "post_call",
"guardrailIdentifier": "test-guardrail",
"guardrailVersion": "DRAFT",
"contextual_grounding_from_messages": True,
"aws_access_key_id": "test-access-key",
"aws_secret_access_key": "test-secret-key",
"aws_region_name": "us-east-1",
"aws_bedrock_runtime_endpoint": "https://bedrock.example",
**({"outputScope": scope} if scope is not None else {}),
}
)
guardrail: Final = initialize_bedrock(params, Guardrail(guardrail_name="grounding", litellm_params=params))
assessment: Final = {
"contextualGroundingPolicy": {
"filters": [{"type": "GROUNDING", "score": 0.96, "threshold": 0.8, "detected": False, "action": "NONE"}]
}
}
def provider(request: httpx.Request) -> httpx.Response:
body: Final = json.loads(request.content)
return httpx.Response(
200,
json={"action": "NONE", "assessments": [assessment] if body.get("outputScope") == "FULL" else []},
)
route: Final = respx_mock.post("https://bedrock.example/guardrail/test-guardrail/version/DRAFT/apply").mock(
side_effect=provider
)
request_data: Final = {
"messages": [
{"role": "system", "content": "The tower is 330 metres tall."},
{"role": "user", "content": "How tall is the tower?"},
]
}
result: Final = await guardrail.apply_guardrail(
inputs={"texts": ["The tower is 330 metres tall."]}, request_data=request_data, input_type="response"
)
sent: Final = json.loads(route.calls.last.request.content)
if scope is None:
assert "outputScope" not in sent
else:
assert sent["outputScope"] == scope
assert sent["source"] == "OUTPUT"
assert result["texts"] == ["The tower is 330 metres tall."]
recorded: Final = request_data["metadata"]["standard_logging_guardrail_information"][-1]["guardrail_response"]
assert recorded["assessments"] == ([assessment] if scope == "FULL" else [])
@pytest.mark.asyncio
async def test_during_call_hook_skips_bedrock_call_for_tool_result_only_turn():
"""A tool_result-only latest user turn must not post an empty content list to Bedrock.
@ -1805,7 +1864,11 @@ async def test_make_bedrock_api_request_logging_event_type_for_spend_logs():
@pytest.mark.asyncio
async def test_make_bedrock_api_request_filters_dynamic_evaluation_overrides():
guardrail = BedrockGuardrail(guardrailIdentifier="test-guardrail", guardrailVersion="DRAFT")
guardrail = BedrockGuardrail(
guardrailIdentifier="test-guardrail",
guardrailVersion="DRAFT",
outputScope=BedrockGuardrailOutputScope.INTERVENTIONS,
)
mock_credentials = MagicMock()
mock_credentials.access_key = "test-access-key"
mock_credentials.secret_key = "test-secret-key"

View file

@ -27607,6 +27607,11 @@ export interface components {
/** Entities */
entities: components["schemas"]["BedrockChecksSensitiveInformationEntityItem"][];
};
/**
* BedrockGuardrailOutputScope
* @enum {string}
*/
BedrockGuardrailOutputScope: "INTERVENTIONS" | "FULL";
/** BillingPeriod */
BillingPeriod: {
/**
@ -37417,6 +37422,8 @@ export interface components {
only_scan_new_messages: boolean | null;
/** @description Optional parameters for the guardrail */
optional_params?: components["schemas"]["CiscoAIDefenseGuardrailConfigModelOptionalParams"] | null;
/** @description ApplyGuardrail response scope. FULL includes detected and non-detected findings. Omit to retain Bedrock's default of returning detected findings only. */
outputScope?: components["schemas"]["BedrockGuardrailOutputScope"] | null;
/**
* Output Parse Pii
* @description When True, LiteLLM will replace the masked text with the original text in the response