fix(extensions): surface content filter refusals on the any_llm chat path - #4234
Merged
seratch merged 1 commit intoAug 6, 2026
Merged
Conversation
A provider that blocks a non-streaming turn can signal it only with finish_reason="content_filter" and an otherwise empty message. The any_llm chat completions path converted that message into zero output items, so callers saw an indistinguishable empty turn. Both sibling paths already handle it, the OpenAI Chat Completions model in openai_chatcompletions.py and LitellmModel, and any_llm streaming already gets it from the shared ChatCmplStreamHandler. Mirror the OpenAI check so the refusal survives on the non-streaming path too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Some providers signal a blocked non-streaming completion only with
finish_reason="content_filter"and an otherwise empty message, with norefusalfield set. On the any_llm chat completions path that message converts to zero output items, so the caller sees a turn that is indistinguishable from a normal empty response and an agent loop can retry it forever.Both siblings already handle this.
OpenAIChatCompletionsModel.get_responseinsrc/agents/models/openai_chatcompletions.pysynthesizes a refusal for exactly this shape, andLitellmModeldoes the same for its provider-specific field. The any_llm streaming path is already covered too, because it delegates to the sharedChatCmplStreamHandler, which raises the same refusal on a terminalcontent_filterchunk. Only the any_llm non-streaming path was missing it, so the same provider behaves differently depending on whether you stream.This copies the OpenAI check verbatim, including its guards: the refusal is synthesized only when the message has no content, no existing refusal, and no tool calls, so a
content_filterthat still carried output is left alone. Two regression tests cover both cases. The empty one fails onmainwithAssertionError: expected a refusal item, got: []and passes with this change, and the non-empty one passes either way to pin the guard.make lintandmake typecheck(mypy and pyright, 0 errors) are green, andtests/models/test_any_llm_model.pyis 55 passed.