fix(protocols): always serialize chat message content (null when absent)#8372
Merged
Conversation
When the reasoning parser (e.g. gpt_oss) produces only reasoning_content
with no final-channel text or content parts, the DeltaChoice->ChatChoice
conversion was setting content to None. Combined with the
skip_serializing_if on ChatCompletionResponseMessage.content, this dropped
the content key entirely from the non-streaming /v1/chat/completions JSON
response, breaking clients that expect both content and reasoning_content.
Fall through to Some(Text("")) when reasoning_content is present so the
key always survives serialization, matching the OpenAI convention of
returning content: "" rather than omitting the field.
Fixes: DGH-651
Closes: #7154
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
WalkthroughModified the Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ting Replace the reasoning-only workaround in aggregator.rs with a direct fix at the serialization config: remove skip_serializing_if from ChatCompletionResponseMessage.content so the key is always present in the JSON (as null when None), matching the upstream OpenAI API shape. This also fixes the tool-call-only response path, which previously dropped the content key the same way. Verified by running the full workspace test suite (2000+ tests, 0 failures). Update the DGH-651 regression test to assert content serializes as serde_json::Value::Null rather than an empty string. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
richardhuo-nv
approved these changes
Apr 20, 2026
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.
Summary
Non-streaming
/v1/chat/completionsresponses were dropping thecontentkey entirely from the JSONmessageany timecontentwasNone(reasoning-only responses, tool-call-only responses). This broke clients like the one in DGH-651 that readresponse.json()['choices'][0]['message']['content'].Root cause:
ChatCompletionResponseMessage.contentcarried#[serde(skip_serializing_if = "Option::is_none")], soNonewas omitted instead of serialized asnull. Upstream OpenAI's API always emits thecontentkey (asnullwhen there is no content) alongsidereasoning_contentortool_calls.Fix: remove
skip_serializing_iffrom that one field. Nowcontent: nullis always emitted when there's no text/content-parts, matching OpenAI's wire format.Approach history:
contenttoSome(Text(""))whenreasoning_contentwas present). That was a semantic muddle ("no content" vs "empty content") and only fixed the reasoning case, not the tool-call-only case.Fixes: DGH-651
Closes #7154
Test plan
test_reasoning_only_response_serializes_content_key_as_nullassertscontentserializes asserde_json::Value::Nullwhen reasoning-only.mainwithout the fix, passes with it.```
cargo test --workspace --lib
all passing
```
🤖 Generated with Claude Code