Skip to content

fix(extensions): surface content filter refusals on the any_llm chat path - #4234

Merged
seratch merged 1 commit into
openai:mainfrom
adityasingh2400:fix-anyllm-content-filter-refusal
Aug 6, 2026
Merged

fix(extensions): surface content filter refusals on the any_llm chat path#4234
seratch merged 1 commit into
openai:mainfrom
adityasingh2400:fix-anyllm-content-filter-refusal

Conversation

@adityasingh2400

Copy link
Copy Markdown
Contributor

Some providers signal a blocked non-streaming completion only with finish_reason="content_filter" and an otherwise empty message, with no refusal field 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_response in src/agents/models/openai_chatcompletions.py synthesizes a refusal for exactly this shape, and LitellmModel does the same for its provider-specific field. The any_llm streaming path is already covered too, because it delegates to the shared ChatCmplStreamHandler, which raises the same refusal on a terminal content_filter chunk. 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_filter that still carried output is left alone. Two regression tests cover both cases. The empty one fails on main with AssertionError: expected a refusal item, got: [] and passes with this change, and the non-empty one passes either way to pin the guard. make lint and make typecheck (mypy and pyright, 0 errors) are green, and tests/models/test_any_llm_model.py is 55 passed.

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.
@seratch seratch changed the title fix(models): surface content filter refusals on the any_llm chat path fix(extensions): surface content filter refusals on the any_llm chat path Aug 5, 2026
@seratch seratch added this to the 0.20.x milestone Aug 6, 2026
@seratch
seratch merged commit 7c6ff9a into openai:main Aug 6, 2026
10 checks passed
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.

2 participants