Skip to content

fix(chatcmpl): surface content-filter refusals when buffering streamed tool calls#3897

Open
gunjanjaswal wants to merge 3 commits into
openai:mainfrom
gunjanjaswal:fix/buffered-stream-content-filter-refusal
Open

fix(chatcmpl): surface content-filter refusals when buffering streamed tool calls#3897
gunjanjaswal wants to merge 3 commits into
openai:mainfrom
gunjanjaswal:fix/buffered-stream-content-filter-refusal

Conversation

@gunjanjaswal

@gunjanjaswal gunjanjaswal commented Jul 20, 2026

Copy link
Copy Markdown

#3769 made a content-filtered stream surface a ResponseOutputRefusal instead of an empty turn, but only on the non-buffered path. With buffer_streamed_tool_calls=True the refusal never fires.

buffer_tool_call_stream forwards a choice only when its delta carries output. A terminal chunk like Choice(index=0, delta=ChoiceDelta(), finish_reason="content_filter") has an empty delta, so it gets dropped before the handler ever sees it. saw_content_filter is never set and the synthesis block added in #3769 never runs. The caller gets a silently empty turn, which is the exact failure mode that PR set out to fix.

The fix forwards a delta-stripped copy of a content-filtered terminal choice, so the finish_reason reaches the handler while buffering semantics stay unchanged.

Reachable through OpenAIChatCompletionsModel, and also via MultiProvider(openai_buffer_streamed_tool_calls=True) and OpenAIProvider.

Tests mirror the content-filter cases from test_litellm_chatcompletions_stream.py with buffering on, plus coverage of the buffering layer itself and a guard against duplicating the tool_calls finish. The gap went untested because LiteLLM never buffers. Checked against upstream/main that the new tests fail without the source change and pass with it. Full suite green (5354 passed), ruff and mypy clean.

The tool-call buffering layer only forwarded a choice when its delta
carried output, so a terminal chunk with an empty delta was dropped
before reaching the stream handler. With buffer_streamed_tool_calls
enabled, a provider that signals a safety block via
finish_reason == "content_filter" and an empty delta produced a silently
empty turn instead of a ResponseOutputRefusal, and "length" was dropped
the same way, making a truncated turn indistinguishable from a short
answer.

Forward a delta-stripped copy of a terminal choice so the finish_reason
reaches the handler while buffering semantics stay unchanged.
"tool_calls" is excluded because the synthesized buffered chunk already
carries that finish_reason.

@seratch seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Before merging, please narrow the passthrough condition and related tests to finish_reason == "content_filter". The current patch also forwards length and stop, but handle_stream() does not interpret either value, so those cases do not change the normalized events or completed response. Please remove those claims and cases while retaining the end-to-end content-filter coverage and the tool_calls non-duplication test. After that focused change, this should be ready for another review.

handle_stream() does not interpret finish_reason "length" or "stop", so
forwarding those terminal choices changed nothing observable while widening
the surface of the fix. Restrict the passthrough to "content_filter", which is
the only value the handler acts on, and drop the corresponding test cases.
@gunjanjaswal

Copy link
Copy Markdown
Author

Good call, narrowed it to content_filter only. You are right that handle_stream() never interprets length or stop, so forwarding those was widening the change for nothing observable.

Dropped the two parametrized cases along with them and removed the length claim from the description. The end-to-end content-filter coverage and the tool_calls non-duplication test are still there. Verified against upstream/main that the remaining tests fail without the handler change.

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