Skip to content

Conversation

@AlanPonnachan
Copy link
Contributor

Unified Content Filtering Exception Handling

This PR closes #1035

Standardizing how content filtering events are handled across different model providers.

Previously, triggering a content filter resulted in inconsistent behaviors: generic ModelHTTPError (Azure), UnexpectedModelBehavior (Google), or silent failures depending on the provider. This PR introduces a dedicated exception hierarchy to allow users to catch and handle prompt refusals and response interruptions programmatically and consistently.

Key Changes:

  • New Exceptions: Added ContentFilterError (base), PromptContentFilterError (for input rejections, e.g., Azure 400), and ResponseContentFilterError (for output refusals).
  • OpenAI & Azure: Updated logic to raise PromptContentFilterError for Azure's specific 400 error body and ResponseContentFilterError when finish_reason='content_filter'.
  • Google Gemini: Updated _process_response to raise ResponseContentFilterError instead of UnexpectedModelBehavior when safety thresholds are triggered.
  • Anthropic: Added mapping for refusal stop reasons to raise ResponseContentFilterError.
  • Tests: Added comprehensive tests covering synchronous and streaming scenarios for OpenAI, Google, and Anthropic in tests/models/.

Example Usage:

from pydantic_ai import Agent
from pydantic_ai.exceptions import ContentFilterError

agent = Agent('openai:gpt-4o')

try:
    # If the prompt or generation triggers a safety filter
    await agent.run("Generate unsafe content...")
except ContentFilterError as e:
    # Catches both PromptContentFilterError and ResponseContentFilterError
    print(f"Request halted by safety filter: {e}")

@rahim-figs
Copy link

Is it possible to handle AWS Bedrock as well? Thanks.

Copy link
Collaborator

@dsfaccini dsfaccini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @AlanPonnachan thank you for the PR! I've requested a couple small changes, let me know if you have any questions

@dsfaccini
Copy link
Collaborator

one more thing I missed, please include the name of the new exception in the fallback model docs

ModelAPIError, which includes ModelHTTPError and ...

@AlanPonnachan
Copy link
Contributor Author

@dsfaccini Thank you for the review. I’ve made the requested changes.

@dsfaccini
Copy link
Collaborator

hey @AlanPonnachan thanks a lot for your work! It looks very good now, I requested a couple more changes but once that's done and coverage passes I think the PR will be ready for merge.

@AlanPonnachan
Copy link
Contributor Author

@dsfaccini Thanks again for the review! I’ve applied the requested changes. Test coverage is now at 100%.

Copy link
Collaborator

@DouweM DouweM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlanPonnachan Thanks for working on this! My main concern is that this doesn't actually make it consistent for all models that respond with finish_reason=='content_filter', just for Anthropic/Google/OpenAI.

self.message = message


class PromptContentFilterError(ContentFilterError):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name this RequestContentFilterError for consistency with our ModelRequest/ModelResponse

"""Raised when the prompt triggers a content filter."""

def __init__(self, status_code: int, model_name: str, body: object | None = None):
message = f"Model '{model_name}' content filter was triggered by the user's prompt"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or by the instructions, right? I'd prefer it to be a bit more generic

provider_details = {'finish_reason': raw_finish_reason}
finish_reason = _FINISH_REASON_MAP.get(raw_finish_reason)
if finish_reason == 'content_filter':
raise ResponseContentFilterError(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do this in ModelRequestNode, so that it'll work for all models, not just Anthropic.

That would mean that it wouldn't trigger FallbackModel to try another model, but that will be possible once we do #3640.

if finish_reason == 'content_filter' and raw_finish_reason:
raise UnexpectedModelBehavior(
f'Content filter {raw_finish_reason.value!r} triggered', response.model_dump_json()
raise ResponseContentFilterError(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change for users that currently have except UnexpectedModelBehavior. So if we add a new exception, I think it should be a subclass of UnexpectedModelBehavior, not ModelAPIError (as it isn't really an API/HTTP/connection error).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combined with the above, the solution may be to remove this check here, and to implement the same if response.finish_reason == 'content_filter' check in ModelRequestNode that will apply to all models.

If we want to get some details from the response up to that level, we can store them in response.provider_details

if (error := body_dict.get('error')) and isinstance(error, dict):
error_dict = cast(dict[str, Any], error)
if error_dict.get('code') == 'content_filter':
raise PromptContentFilterError(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about instead handling this by returning a ModelResponse with empty parts and finish_reason == 'content_filter', so that it can go through the same logic as the Anthropic and Google models?

I also don't think they distinguish between input/request and output/response content filter errors, so I don't think we need to here either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Content Filtering Exception Handling

4 participants