Skip to content

Fix crash when scorer LLM response is blocked by content filter#2072

Open
hannahwestra25 wants to merge 13 commits into
microsoft:mainfrom
hannahwestra25:hannahwestra25/fix-scorer-content-filter-crash
Open

Fix crash when scorer LLM response is blocked by content filter#2072
hannahwestra25 wants to merge 13 commits into
microsoft:mainfrom
hannahwestra25:hannahwestra25/fix-scorer-content-filter-crash

Conversation

@hannahwestra25

Copy link
Copy Markdown
Contributor

When the scorer's own LLM call is blocked by Azure content filtering (e.g., because it's grading harmful content), the response contains only an error piece with no text piece. The next() generator in _score_value_with_llm_async raises StopIteration, which becomes a RuntimeError inside the async coroutine.

This adds an explicit check before the next() call and raises a descriptive BadRequestException suggesting the user configure a scorer endpoint without content filtering.

hannahwestra25 and others added 3 commits June 23, 2026 21:07
When the scorer's own LLM call is blocked by Azure content filtering,
the response contains only an error piece with no text piece. The
next() call in _score_value_with_llm_async raises StopIteration, which
becomes a RuntimeError inside the async coroutine.

Add an explicit check for all-blocked response pieces before the next()
call and raise a descriptive BadRequestException instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread pyrit/score/scorer.py Outdated
# Require every piece to be blocked (all) rather than any: a partially blocked
# response may still carry a usable text piece, so we only bail out when there
# is no salvageable content to parse.
if all(piece.is_blocked() for piece in response[0].message_pieces):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

When a piece is blocked, every piece is marked blocked. The piece's converted_value has the error JSON. There is partial content in the metadata, but no non-blocked text piece.

So this should be updated because it will never get partial content.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good catch, you're right. A content-filter block always comes back as a single fully-blocked error piece (partial content lives in prompt_metadata, not a separate text piece), so the old comment's salvageable text piece reasoning was wrong. Fixed the comment in 0aa44b9 to just say a block yields a single error piece with no parseable text. The all() check is functionally equivalent to checking the first piece here; kept it as a harmless defensive check rather than changing behavior.

Copilot AI and others added 2 commits July 1, 2026 04:56
A content-filter block always yields a single fully-blocked error piece, so the previous comment's claim about a salvageable text piece was inaccurate.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…scoring

The scoring transport was refactored into _run_llm_scoring_async
(pyrit/score/llm_scoring.py) with parsing delegated to ResponseHandler.
Move the all-pieces-blocked BadRequestException guard from the old inline
_score_value_with_llm_async body into the new transport helper, right before
the text-piece extraction that previously crashed on fully-blocked responses.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@rlundeen2

Copy link
Copy Markdown
Contributor

I'm going to work on this and take it over since Hannah is out

@rlundeen2 rlundeen2 self-assigned this Jul 10, 2026
rlundeen2 and others added 5 commits July 10, 2026 14:02
When the scorer's own LLM response is content-filtered, the transport now raises
a dedicated ScorerLLMResponseBlockedException instead of a generic
BadRequestException. The raise-vs-fallback policy lives in the Scorer (per
framework.md): score_async catches the exception and, when
raise_if_scorer_blocks is False, returns the scorer's type default (False / 0.0)
via _build_fallback_score instead of raising. Defaults to raising.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…m policy

Use 'scorer's LLM response' consistently in llm_scoring.py and drop the
Scorer.raise_if_scorer_blocks reference from the transport docstring/comment so
the transport doesn't name the Scorer's policy attribute.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The crash this PR fixes is a StopIteration when no text piece exists in the
scorer's response. The previous guard keyed on 'all pieces blocked', which left
two gaps: an empty piece list spuriously raised (all() over empty is True), and
a non-blocked response with no text piece fell through and crashed again.

Resolve the text piece safely and branch on why it's missing: a content-filter
block raises ScorerLLMResponseBlockedException (routed through the Scorer's
raise_if_scorer_blocks policy), while any other no-text response raises
EmptyResponseException so a genuinely empty/malformed response surfaces as a real
error instead of being silently converted to a default score.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants