fix(openai): apply empty-assistant filter to streaming path#7758
Conversation
…trBotDevs#7721) PR AstrBotDevs#7202 added empty-assistant filtering in `_query` so strict providers (Moonshot, etc.) wouldn't 400 on history with blank assistant entries. The streaming sibling `_query_stream` was never updated, so DeepSeek Reasoner — which returns reasoning only during tool calls, leaving serialized content as `""` — blew up with `Invalid assistant message: content or tool_calls must be set` on the next turn. Hoisted the filter into a `_sanitize_assistant_messages` helper and called it from both paths. Also widened the empty check to cover `content == []`, which the original filter missed and which shows up with providers that emit content as a list of parts.
There was a problem hiding this comment.
Code Review
This pull request refactors assistant message sanitization into a centralized static method, _sanitize_assistant_messages, and ensures it is applied to both standard and streaming query paths. This change prevents 400 errors from strict APIs (like Moonshot or DeepSeek) when assistant messages lack both content and tool calls. Additionally, regression tests were added to verify the fix for streaming and to ensure empty list content is correctly handled. A review comment suggests simplifying the loop logic within the new sanitization method to reduce nesting and improve readability.
| if not isinstance(msg, dict) or msg.get("role") != "assistant": | ||
| cleaned.append(msg) | ||
| continue | ||
|
|
||
| content = msg.get("content") | ||
| tool_calls = msg.get("tool_calls") | ||
|
|
||
| if _is_empty(content) and not tool_calls: | ||
| logger.warning(f"过滤第 {idx} 条空 assistant 消息 (无工具调用)") | ||
| continue | ||
|
|
||
| if _is_empty(content) and tool_calls: | ||
| msg["content"] = None | ||
|
|
||
| cleaned.append(msg) |
There was a problem hiding this comment.
The logic inside this loop can be simplified by inverting the initial if condition. This removes one level of indentation and a continue statement, making the main logic path for assistant messages clearer and improving readability.
if isinstance(msg, dict) and msg.get("role") == "assistant":
content = msg.get("content")
tool_calls = msg.get("tool_calls")
if _is_empty(content) and not tool_calls:
logger.warning(f"过滤第 {idx} 条空 assistant 消息 (无工具调用)")
continue
if _is_empty(content) and tool_calls:
msg["content"] = None
cleaned.append(msg)
Fixes #7721.
PR #7202 sanitized empty assistant messages in
_queryso strict OpenAI-compatible providers (Moonshot, etc.) wouldn't 400 on history rebuilt with blank assistant entries. The streaming sibling_query_streamwas never updated, so DeepSeek Reasoner — which emits reasoning-only chunks during tool calls and serializes the assistant turn withcontent=""— blows up on the next user turn with:Changes
_sanitize_assistant_messages(payloads)and call it from both_queryand_query_streamright beforechat.completions.create(...).content == [](list-of-parts format), which the original filter missed — caught by a new test.Tests
Added two regression tests to
tests/test_openai_source.py:test_query_stream_filters_empty_assistant_message— monkeypatcheschat.completions.createin streaming mode, sends a history with a{"role": "assistant", "content": ""}entry, asserts it's dropped from the payload the client actually sends.test_query_filters_empty_list_content_assistant_message— same idea but withcontent=[], covering the gap reporter flagged.Existing
test_query_filters_empty_assistant_message_without_tool_callsand the null-content variant still pass — they now exercise the helper via_query.Summary by Sourcery
Apply consistent sanitization of empty assistant messages for both standard and streaming OpenAI-compatible requests to prevent strict providers from returning 400 errors.
Bug Fixes:
_query_streampath so histories with reasoning-only assistant turns do not cause subsequent 400 errors.Enhancements:
_sanitize_assistant_messageshelper shared by both_queryand_query_stream.Tests:
_query_streamfilters empty assistant messages from the payload sent to the OpenAI-compatible client.content == []are treated as empty and removed when appropriate.