mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-08 22:21:35 +00:00
fix(guardrails): forward on_unscannable_image through initialize_bedrock
initialize_bedrock enumerates its kwargs explicitly, so the new setting parsed and rendered but never reached the guardrail. An operator who opted into "allow" kept getting 400s on unscannable images with nothing to explain why. Same shape as the chunk_budget_chars regression, so the test follows that one and asserts through initialize_guardrail rather than the constructor. Also trims the docstrings added by this branch down to the rationale that is not already obvious from the code, and drops a stale line describing a return convention these helpers no longer use. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c98cf95b5
commit
a19dd2d92e
3 changed files with 47 additions and 30 deletions
|
|
@ -338,13 +338,7 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
return cleaned or None
|
||||
|
||||
async def _create_bedrock_input_content_request(self, messages: list[AllMessageValues] | None) -> BedrockRequest:
|
||||
"""
|
||||
Create a bedrock request for the input content - the LLM request.
|
||||
|
||||
Text and image parts are both sent, so a guardrail with the IMAGE modality
|
||||
enabled inspects the image the caller actually sent instead of only the text
|
||||
that happened to sit next to it.
|
||||
"""
|
||||
"""Create a bedrock request for the input content - the LLM request."""
|
||||
bedrock_request: Final[BedrockRequest] = BedrockRequest(source="INPUT")
|
||||
if messages is None:
|
||||
return bedrock_request
|
||||
|
|
@ -357,10 +351,8 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
async def _build_input_content_items(self, message: AllMessageValues) -> tuple[BedrockContentItem, ...]:
|
||||
"""Flatten one request message into ApplyGuardrail INPUT content items.
|
||||
|
||||
INPUT scans send text and image parts. Grounding qualifiers are attached
|
||||
exclusively when assembling the OUTPUT request, so a caller cannot use a
|
||||
grounding_source/query tag to change how input-safety policies treat their
|
||||
content (which would be an input-guardrail bypass).
|
||||
Grounding qualifiers are attached only when assembling the OUTPUT request, so a
|
||||
grounding_source/query tag cannot change how input-safety policies treat content
|
||||
"""
|
||||
content = message.get("content")
|
||||
if content is None:
|
||||
|
|
@ -381,11 +373,9 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
if not isinstance(item, dict):
|
||||
return None
|
||||
part: Final = cast(Mapping[str, object], item) # cast-ok: narrowed to dict on the line above
|
||||
# Classify by the declared type before reading any field. Provider
|
||||
# transformations branch on `type`, so a part tagged image_url reaches the
|
||||
# model as an image even when it also carries a `text` key. Reading `text`
|
||||
# first would scan that decoy and forward the image unscanned, which is the
|
||||
# bypass this whole extractor exists to close.
|
||||
# Provider transformations branch on `type`, so an image_url part reaches the
|
||||
# model as an image even when it also carries `text`. Reading `text` first would
|
||||
# scan that decoy and forward the image unscanned
|
||||
if part.get("type") == "image_url":
|
||||
image_url: Final = self._get_image_url(item=part)
|
||||
if image_url is None:
|
||||
|
|
@ -409,15 +399,10 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
return None
|
||||
|
||||
def _handle_unscannable_image(self, reason: str) -> None:
|
||||
"""Decide what to do with an image part ApplyGuardrail cannot scan.
|
||||
"""Block or warn for an image part ApplyGuardrail cannot scan.
|
||||
|
||||
The image still reaches the model either way, so skipping it silently would
|
||||
let a caller defeat an IMAGE-modality guardrail just by picking a format the
|
||||
API does not accept. `on_unscannable_image` defaults to "block" for that
|
||||
reason; "allow" restores the permissive behavior for deployments that would
|
||||
rather serve the request than fail it.
|
||||
|
||||
Returns None so callers can `return self._handle_unscannable_image(...)`.
|
||||
The image reaches the model either way, so skipping it silently would let a
|
||||
caller defeat an IMAGE-modality guardrail by picking a format the API rejects
|
||||
"""
|
||||
if self.on_unscannable_image == "block":
|
||||
raise HTTPException(
|
||||
|
|
@ -440,13 +425,10 @@ class BedrockGuardrail(CustomGuardrail, BaseAWSLLM):
|
|||
)
|
||||
|
||||
async def _build_image_content_item(self, image_url: str) -> BedrockContentItem | None:
|
||||
"""Decode/fetch an image part into an ApplyGuardrail image block.
|
||||
"""Decode or fetch an image part into an ApplyGuardrail image block.
|
||||
|
||||
Remote URLs are only fetched while LiteLLM's URL validation is on. With
|
||||
validation disabled `async_safe_get` degrades to an unrestricted, redirect
|
||||
following GET, and the URL comes straight from the caller, so fetching here
|
||||
would turn the guardrail into an SSRF primitive. Such an image is treated as
|
||||
unscannable instead.
|
||||
With `user_url_validation` off, `async_safe_get` degrades to an unrestricted,
|
||||
redirect-following GET on a caller-supplied URL, so it is not fetched at all
|
||||
"""
|
||||
if not image_url.startswith("data:") and not getattr(litellm, "user_url_validation", True):
|
||||
self._handle_unscannable_image(
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ def initialize_bedrock(litellm_params: LitellmParams, guardrail: Guardrail):
|
|||
prompt_attack_threshold=litellm_params.prompt_attack_threshold,
|
||||
pii_confidence_threshold=litellm_params.pii_confidence_threshold,
|
||||
chunk_budget_chars=litellm_params.chunk_budget_chars,
|
||||
on_unscannable_image=litellm_params.on_unscannable_image,
|
||||
default_on=litellm_params.default_on,
|
||||
disable_exception_on_block=litellm_params.disable_exception_on_block,
|
||||
mask_request_content=litellm_params.mask_request_content,
|
||||
|
|
|
|||
|
|
@ -118,3 +118,37 @@ def test_initialize_guardrail_sets_run_in_parallel(config_value, expected):
|
|||
|
||||
custom_guardrail = guardrail_handler.guardrail_id_to_custom_guardrail[result["guardrail_id"]]
|
||||
assert custom_guardrail.run_in_parallel is expected
|
||||
|
||||
|
||||
def test_initialize_bedrock_forwards_on_unscannable_image():
|
||||
"""Regression: `on_unscannable_image` set in config.yaml must reach the guardrail.
|
||||
|
||||
Same shape as chunk_budget_chars above: the field lives on
|
||||
BedrockGuardrailConfigModel so LitellmParams parses it, but initialize_bedrock
|
||||
enumerates its kwargs explicitly. Dropped here, an operator who opted into
|
||||
`allow` would keep getting 400s on unscannable images with no indication why.
|
||||
"""
|
||||
import litellm
|
||||
from litellm.proxy.guardrails.guardrail_hooks.bedrock_guardrails import BedrockGuardrail
|
||||
|
||||
test_guardrail = {
|
||||
"guardrail_name": "test_bedrock_unscannable_image",
|
||||
"litellm_params": {
|
||||
"guardrail": SupportedGuardrailIntegrations.BEDROCK.value,
|
||||
"mode": "pre_call",
|
||||
"guardrailIdentifier": "test-guardrail",
|
||||
"guardrailVersion": "DRAFT",
|
||||
"on_unscannable_image": "allow",
|
||||
},
|
||||
}
|
||||
|
||||
guardrail_handler = InMemoryGuardrailHandler()
|
||||
guardrail_handler.initialize_guardrail(guardrail=test_guardrail)
|
||||
|
||||
initialized = [
|
||||
callback
|
||||
for callback in litellm.callbacks
|
||||
if isinstance(callback, BedrockGuardrail) and callback.guardrail_name == "test_bedrock_unscannable_image"
|
||||
]
|
||||
assert initialized, "bedrock guardrail was not registered as a callback"
|
||||
assert initialized[-1].on_unscannable_image == "allow"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue