diff --git a/litellm/proxy/guardrails/guardrail_endpoints.py b/litellm/proxy/guardrails/guardrail_endpoints.py index afb9997f2e6..822f2ad927e 100644 --- a/litellm/proxy/guardrails/guardrail_endpoints.py +++ b/litellm/proxy/guardrails/guardrail_endpoints.py @@ -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) diff --git a/litellm/types/guardrails.py b/litellm/types/guardrails.py index 69cb88bfa2f..8707a52a73d 100644 --- a/litellm/types/guardrails.py +++ b/litellm/types/guardrails.py @@ -588,6 +588,10 @@ class BedrockGuardrailStreamingParams(BaseModel): ) +class BedrockGuardrailUIConfigModel(BedrockGuardrailConfigModel, BedrockGuardrailStreamingParams): + pass + + class LakeraV2GuardrailConfigModel(BaseModel): """Configuration parameters for the Lakera AI v2 guardrail""" diff --git a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py index 530f8ffd854..ccc7ede941e 100644 --- a/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py +++ b/tests/test_litellm/proxy/guardrails/test_guardrail_endpoints.py @@ -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