fix(completions): replay reasoning_content on thinking-mode tool calls - #48
Merged
Fodesu merged 1 commit intoSep 8, 2026
Merged
Conversation
Fodesu
force-pushed
the
worktree-fix-completions-thinking-replay
branch
from
September 7, 2026 12:50
d991950 to
4e04c51
Compare
DeepSeek thinking mode rejects a request that carries tools when an assistant tool-call message after the last user turn has no reasoning_content key (400 "must be passed back to the API"). An empty string passes. Kimi enforces the same rule. - Response: reasoning_content is decoded as *string. A present-but-empty key yields a ReasoningPart with empty text; the stream opens one block on the first "" delta and never reopens a closed block. - Request: any openai-chat-v1 ReasoningPart is sent, "" included. padThinkingReplay adds an empty key to tool-call messages without a ReasoningPart (older persisted history) under DeepSeek/Kimi compat or when the request already carries reasoning_content. Plain OpenAI and MiniMax are left untouched. Verified against deepseek-v4-flash / v4-pro in stream and non-stream mode.
Fodesu
force-pushed
the
worktree-fix-completions-thinking-replay
branch
from
September 7, 2026 12:56
4e04c51 to
ffcced4
Compare
Closed
1 task
Fodesu
marked this pull request as ready for review
September 7, 2026 16:15
Fodesu
force-pushed
the
worktree-fix-completions-thinking-replay
branch
2 times, most recently
from
September 8, 2026 09:38
1866679 to
ffcced4
Compare
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.
The bug
DeepSeek in thinking mode rejects a request with a hard 400 when it carries tools and an assistant tool-call message after the last user turn has no
reasoning_contentkey:An empty string passes. A missing key or
nulldoes not. Kimi enforces the same rule.We hit this only on continuations of persisted history (resume after a tool approval), never inside a single run. DeepSeek fills the key itself while the
tool_callid is fresh, so the gap shows up once the id has expired. Two things in the provider caused the missing key:reasoning_contentwas decoded asstring, so""(DeepSeek's value for a thinking step that produced no reasoning) was indistinguishable from the key being absent, and noReasoningPartwas recorded for that step.reasoning_contentwasomitemptyon astring, so an empty reasoning block was dropped from the wire.The fix
*string. A present-but-empty key yields aReasoningPartwith empty text; the stream opens one block on the first""delta and never reopens a closed one. Anyopenai-chat-v1part is sent back,""included.padThinkingReplayadds an empty key to tool-call messages that carry noReasoningPartat all (history persisted before this change) when DeepSeek or Kimi compat is on, or when the request already carriesreasoning_content. Plain OpenAI and MiniMax are untouched, since OpenAI rejects unknown message fields.Verification
Three tests: padding conditions (table), stream markers (table), and one two-step run with an empty thinking step that checks both the recorded
ReasoningPartand the replayed key.Live check against
deepseek-v4-flash, replaying a persisted history whose tool call carries a client-generated id, so DeepSeek cannot fill the key itself. The first row reproduces the bug, the rest cover both sides of the fix:ReasoningPartreasoning_content ... must be passed backReasoningPartReasoningPartReasoningPartKnown limitation
Through a non-official base URL with no compat option set, padding only kicks in once the conversation already carries
reasoning_content. ConfigureWithDeepSeekChatCompletionsCompat()for proxies. A runtime fallback (retry once on the specific 400) is deferred.