Repository navigation
fix(provider/compatible): preserve reasoning_content for plain-text assistant turns - #6284
Conversation
…ssistant turns Closes #6233. `convert_messages_for_native` had two parsing branches: assistant messages with `tool_calls` (extracts reasoning_content from JSON), and tool-role messages. Plain-text assistant messages whose content is JSON-encoded with `reasoning_content` and no `tool_calls` (the shape DeepSeek V4 thinking-mode produces for non-tool turns) fell through to the generic fallback, which dropped reasoning_content to None and caused a 400 on the next request. Adds a third branch that fires when assistant content parses as JSON without `tool_calls` and carries at least one of `content` or `reasoning_content` keys. Unrelated JSON (no recognized keys) still falls through to the verbatim text path so a stray `{"foo":"bar"}` survives to the wire instead of collapsing to empty. Mirrors the existing `convert_messages_for_native_round_trips_reasoning_content` test for the no-tool-calls shape, plus an unrelated-JSON regression test to lock the fall-through behavior.
Audacity88
left a comment
There was a problem hiding this comment.
Checked PR #6284 at head 99b7701, the full single-file diff, #6233, #6059, #6269, the top-level/inline/formal review threads, CI status, and adjacent source in crates/zeroclaw-providers/src/compatible.rs and the runtime history builders. There are no prior comments or active review blocks, and CI is green. I did not run local cargo; this pass relies on CI, the author's stated validation, and source inspection.
🔴 Blocking — Plain JSON answers with a content key are no longer replayed verbatim
The new branch accepts any assistant message JSON that has either content or reasoning_content and no tool_calls. That fixes the DeepSeek replay envelope when reasoning_content is present, but it also changes ordinary assistant text that happens to be JSON with a content key.
ChatMessage::content is an opaque string, so a model can legitimately answer with structured JSON. For example, a structured-output assistant answer like {"content":"raw"} used to fall through to the plain-text path, so the next request replayed the original JSON string. With this PR, the new branch treats it as a ZeroClaw replay envelope and sends only raw. If the JSON has a non-string content, the branch can return a native assistant message with no content at all. Both cases change conversation history that is not part of the reasoning_content fix.
The narrow fix is to require an actual parsed reasoning_content field, or another explicit replay-envelope signal, before taking this no-tool branch. Please also add a regression test where assistant content is JSON with a content key but no reasoning_content, and assert that it still falls through verbatim. If content-only JSON is intended to become an internal replay envelope too, that is a broader history-format change and needs to be documented and tested separately from this DeepSeek preservation fix.
🟡 Warning — PR body references a stale gate name
The rollback section says this behavior is gated on an existing support_reasoning_content argument, but the current convert_messages_for_native argument is allow_user_image_parts. Please update that text so the risk and rollback notes match the code being reviewed.
🟢 What looks good — the intended #6233 slice is correctly isolated
The PR is otherwise scoped to the right layer for the original #6233 replay bug. The added positive-path test covers the DeepSeek no-tool-calls envelope, and the unrelated-JSON fallback test is the right kind of guardrail; it just needs the content-key structured-output case as well.
…-reasoning-content-fallback
|
I pushed a narrow maintainer follow-up for my earlier review blocker, then refreshed the branch against current The branch now only treats a no-tool assistant JSON message as a thinking-mode replay envelope when Local validation after the follow-up and current- All passed locally. I also refreshed the PR body and removed the stale @WareWolf-MoonWall, could you take a look when you have a chance? |
WareWolf-MoonWall
left a comment
There was a problem hiding this comment.
First review of head c2d0090. I read the PR body, the full diff (single-file, crates/zeroclaw-providers/src/compatible.rs), Audacity88's CHANGES_REQUESTED, Audacity88's follow-up comment, and all four regression tests. CI is 13/13 green.
Audacity88's CHANGES_REQUESTED is still formally active on GitHub. Audacity88 explicitly states in their latest comment: "I think this resolves my previous blocker, but I am not submitting an approval review on a head that now includes my maintainer commit." Because their formal review has not been dismissed, I cannot post an approval here per protocol — but I can confirm what the current code does and clear the path to merge.
✅ Resolved — Audacity88's 🔴 block: plain JSON with content key falls through verbatim
The narrowed guard in the current diff requires reasoning_content to be present and to be a string (.and_then(serde_json::Value::as_str)). An assistant message like {"content": "raw"} has no reasoning_content key; as_str() returns None; the let Some(reasoning_content) = ... binding fails; the branch does not fire; the message falls through to the plain-text path and the original JSON string is preserved verbatim on replay. ✓
The four regression tests pin this correctly:
convert_messages_for_native_round_trips_reasoning_content_without_tool_calls— DeepSeek-style envelope with both fields fires the branch ✓convert_messages_for_native_content_only_json_falls_through—{"content": "raw"}(noreasoning_content) falls through verbatim ✓convert_messages_for_native_non_string_reasoning_content_falls_through—reasoning_content: nullfalls through verbatim ✓convert_messages_for_native_unrelated_json_falls_through—{"foo": "bar"}falls through verbatim ✓
✅ Resolved — Audacity88's 🟡 warning: stale gate name removed from PR body
The rollback section no longer references a support_reasoning_content argument. It describes the behavior correctly: "limited to native compatible-provider history conversion for assistant JSON payloads that carry string reasoning_content." ✓
🟢 What looks good — branch ordering is correct
The new no-tool branch sits after the existing tool_calls branch (which handles JSON with tool_calls) and before the tool role branch and plain-text fallback. Tool-call assistant turns still win at the earlier branch and are unaffected. The ordering is correct and the code comment at the new branch explains the DeepSeek V4 replay requirement clearly.
Path to merge
@Audacity88 — your formal CHANGES_REQUESTED at the prior head is the only remaining gate. If you're satisfied that the current code resolves your block, dismissing your review or posting a new approval at this head would clear the path. Alternatively, another maintainer with review-dismiss permission can dismiss the stale block. Everything I've read on this head is ready.
Resolved by follow-up commits on current head c2d0090; current head reviewed by WareWolf and CI is green.
Audacity88
left a comment
There was a problem hiding this comment.
Approved. I rechecked the current head after the follow-up commits and the stale blocking review was dismissed. The Compatible plain-text replay path now preserves reasoning_content for assistant turns while keeping the existing content fallback behavior, and the regression coverage is aligned with the provider helper behavior.
CI is green on the current head, and the previous blocker is resolved.
…n-text assistant turns (#6284) - 99b7701 fix(provider/compatible): preserve reasoning_content for plain-text assistant turns - 1216907 fix(provider/compatible): preserve structured JSON replies - 3a210d1 Merge remote-tracking branch 'origin/master' into fix/6233-compatible-reasoning-content-fallback - c2d0090 test(provider/compatible): align reasoning replay tests with provider helper e3a3228
…n-text assistant turns (zeroclaw-labs#6284) - 99b7701 fix(provider/compatible): preserve reasoning_content for plain-text assistant turns - 1216907 fix(provider/compatible): preserve structured JSON replies - 3a210d1 Merge remote-tracking branch 'origin/master' into fix/6233-compatible-reasoning-content-fallback - c2d0090 test(provider/compatible): align reasoning replay tests with provider helper e3a3228
…ssistant turns (zeroclaw-labs#6284) - 99b7701 fix(provider/compatible): preserve reasoning_content for plain-text assistant turns - 1216907 fix(provider/compatible): preserve structured JSON replies - 3a210d1 Merge remote-tracking branch 'origin/master' into fix/6233-compatible-reasoning-content-fallback - c2d0090 test(provider/compatible): align reasoning replay tests with provider helper
Summary
master(all contributions)OpenAiCompatibleProvider::convert_messages_for_nativeonly extractedreasoning_contentfrom assistant messages that carriedtool_calls. Plain-text assistant turns from thinking-mode providers such as DeepSeek V4 can JSON-encode{"content": ..., "reasoning_content": ...}with notool_calls, then losereasoning_contenton replay.reasoning_content in the thinking mode must be passed back), breaking multi-turn conversations.tool_calls, has stringreasoning_content, and has absent/null/stringcontent. That preserves the thinking-mode replay payload without treating ordinary structured JSON replies as internal envelopes.reasoning_contentfallback.convert_messages_for_nativeincrates/zeroclaw-providers/src/compatible.rs. No changes to wire format, request building, streaming, tool-call handling, or other providers. The existingtool_callsandtoolbranches are untouched.OpenAiCompatibleProviderthat round-trips assistant history through the native compatible-provider conversion path. Tool-call assistant turns and ordinary structured JSON replies keep their previous behavior; only plain assistant JSON with a stringreasoning_contentis treated as a thinking-mode replay envelope.bug,size: S,risk: high,provider,provider:compatible,provider:deepseekValidation Evidence (required)
Local validation is the signal CI cannot replace. Run the full battery and paste literal output (tails, failures, warnings — not "all passed").
cargo fmt --all -- --check cargo clippy --all-targets -- -D warnings cargo testmaster: the JSON-with-tool_callsbranch still wins for tool turns, the new no-tool branch only triggers for stringreasoning_content, content-only JSON stays verbatim, non-stringreasoning_contentstays verbatim, and unrelated JSON still hits the plain-text fallback. Did not run a live request against DeepSeek V4 thinking; the regression tests cover the request-history conversion path this PR owns.cargo testwas scoped to the affected provider conversion tests because the change is entirely contained inzeroclaw-providersand the focused tests exercise the changed branch plus the guarded fallbacks. Workspace-widecargo clippy --all-targets -- -D warningswas not rerun locally after this maintainer follow-up; the focused provider clippy above covers the touched crate, and GitHub CI covers the full required matrix.Security & Privacy Impact (required)
Yes/No for each. Answer any
Yeswith a 1–2 sentence explanation.No)No)No)No)Yes, describe the risk and mitigation: N/A.Compatibility (required)
Yes) — messages that previously matched thetool_callsbranch or fell through unchanged still take the same path. The new behavior is limited to the previously broken plain-assistant replay shape that carries stringreasoning_content.No)NoorYesto either: exact upgrade steps for existing users: None — transparent fix on rebuild.Rollback (required for
risk: mediumandrisk: high)git revert c2d00908c 12169079a 99b77011dand rebuild. Reverting restores the prior behavior, including the DeepSeek/Kimi replay failure for plain assistant turns withreasoning_content.reasoning_content.reasoning_content in the thinking mode must be passed back. In logs, look for 400 responses from compatible-provider requests after a multi-turn assistant reply.Maintainer note
Refs #6059.
#6059 is being addressed across three PRs covering the layers
reasoning_contentround-trips through:NativeChatRequestso assistant tool-call history preservesreasoning_content.reasoning_contentpreserved through context compression.This PR is the plain-text-assistant piece. It can land independently of #6285. The issue stays open until both open PRs ship and a current DeepSeek/Kimi thinking-mode smoke passes.
— @singlerider, 2026-05-28; updated by @Audacity88 after resolving the structured-output fallback review blocker and refreshing the branch against current
master.