fix(guardrails): expose bedrock streaming controls in Admin UI provider params

Advertise streaming_buffer_until_moderated, streaming_sampling_rate and
streaming_end_of_stream_only for the bedrock guardrail so the Add/Edit
Guardrail form can turn off post_call buffering, and stop marking children
of an optional nested block (checks) as required so the form can be saved
with just a guardrailIdentifier.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-09-10 23:57:52 +00:00
parent ef1a37795c
commit 0f1db14a4c
3 changed files with 42 additions and 4 deletions

View file

@ -36,7 +36,7 @@ from litellm.types.guardrails import (
ApplyGuardrailRequest,
ApplyGuardrailResponse,
BaseLitellmParams,
BedrockGuardrailConfigModel,
BedrockGuardrailUIConfigModel,
Guardrail,
GuardrailEventHooks,
GuardrailInfoResponse,
@ -1840,6 +1840,7 @@ def _build_field_dict(
def _extract_fields_recursive(
model: type[BaseModel],
depth: int = 0,
parent_required: bool = True,
) -> dict[str, object]:
# Check if we've exceeded the maximum recursion depth
if depth > DEFAULT_MAX_RECURSE_DEPTH:
@ -1865,7 +1866,7 @@ def _extract_fields_recursive(
# Get field metadata
description = field.description or field_name
required = field.is_required()
required = parent_required and field.is_required()
# Check if this is a BaseModel subclass
is_basemodel_subclass = (
@ -1876,7 +1877,7 @@ def _extract_fields_recursive(
if is_basemodel_subclass:
# Recursively get fields from the nested model
nested_fields = _extract_fields_recursive(cast(type[BaseModel], field_annotation), depth + 1)
nested_fields = _extract_fields_recursive(cast(type[BaseModel], field_annotation), depth + 1, required)
fields[field_name] = {
"description": description,
"required": required,
@ -1959,7 +1960,7 @@ async def get_provider_specific_params():
```
"""
# Get fields from the models
bedrock_fields: Final = _get_fields_from_model(BedrockGuardrailConfigModel)
bedrock_fields: Final = _get_fields_from_model(BedrockGuardrailUIConfigModel)
presidio_fields: Final = _get_fields_from_model(PresidioPresidioConfigModelUserInterface)
lakera_v2_fields: Final = _get_fields_from_model(LakeraV2GuardrailConfigModel)
tool_permission_fields: Final = _get_fields_from_model(ToolPermissionGuardrailConfigModel)

View file

@ -587,6 +587,10 @@ class BedrockGuardrailStreamingParams(BaseModel):
)
class BedrockGuardrailUIConfigModel(BedrockGuardrailConfigModel, BedrockGuardrailStreamingParams):
pass
class LakeraV2GuardrailConfigModel(BaseModel):
"""Configuration parameters for the Lakera AI v2 guardrail"""

View file

@ -682,6 +682,39 @@ async def test_provider_specific_params_includes_embedding_toggle():
assert field["default_value"] is False
@pytest.mark.asyncio
async def test_provider_specific_params_includes_bedrock_streaming_flags():
"""The Add/Edit Guardrail form only submits params the endpoint advertises, so
the Bedrock streaming knobs must be listed or the UI cannot turn off
post_call buffering (LIT-7534)."""
from litellm.proxy.guardrails.guardrail_endpoints import get_provider_specific_params
provider_params = await get_provider_specific_params()
bedrock = provider_params["bedrock"]
assert bedrock["streaming_buffer_until_moderated"]["type"] == "boolean"
assert bedrock["streaming_buffer_until_moderated"]["default_value"] is True
assert bedrock["streaming_end_of_stream_only"]["type"] == "boolean"
assert bedrock["streaming_end_of_stream_only"]["default_value"] is False
assert bedrock["streaming_sampling_rate"]["type"] == "number"
assert bedrock["streaming_sampling_rate"]["default_value"] == 5
@pytest.mark.asyncio
async def test_provider_specific_params_optional_nested_children_not_required():
"""The form validates every advertised field but never binds nested children,
so a required child under an optional `checks` block makes Save impossible
for any Bedrock guardrail that uses guardrailIdentifier instead."""
from litellm.proxy.guardrails.guardrail_endpoints import get_provider_specific_params
provider_params = await get_provider_specific_params()
checks = provider_params["bedrock"]["checks"]
assert checks["required"] is False
assert checks["fields"]["contentFilter"]["fields"]["categories"]["required"] is False
assert checks["fields"]["sensitiveInformation"]["fields"]["entities"]["required"] is False
@pytest.mark.asyncio
async def test_provider_specific_params_includes_hide_secrets():
"""hide-secrets lives in the enterprise package so it is not in