Skip to content

fix(completions): replay reasoning_content on thinking-mode tool calls - #48

Merged
Fodesu merged 1 commit into
felinics:mainfrom
Fodesu:worktree-fix-completions-thinking-replay
Sep 8, 2026
Merged

fix(completions): replay reasoning_content on thinking-mode tool calls#48
Fodesu merged 1 commit into
felinics:mainfrom
Fodesu:worktree-fix-completions-thinking-replay

Conversation

@Fodesu

@Fodesu Fodesu commented Sep 7, 2026

Copy link
Copy Markdown
Member

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_content key:

400 The reasoning_content in the thinking mode must be passed back to the API

An empty string passes. A missing key or null does 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_call id is fresh, so the gap shows up once the id has expired. Two things in the provider caused the missing key:

  • Response side: reasoning_content was decoded as string, so "" (DeepSeek's value for a thinking step that produced no reasoning) was indistinguishable from the key being absent, and no ReasoningPart was recorded for that step.
  • Request side: reasoning_content was omitempty on a string, so an empty reasoning block was dropped from the wire.

The fix

  • Both sides now use *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 one. Any openai-chat-v1 part is sent back, "" included.
  • padThinkingReplay adds an empty key to tool-call messages that carry no ReasoningPart at all (history persisted before this change) when DeepSeek or Kimi compat is on, or when the request already carries reasoning_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 ReasoningPart and 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:

    History Compat Mode Result
    tool call without ReasoningPart none non-stream 400 reasoning_content ... must be passed back
    tool call with empty ReasoningPart none non-stream 200
    tool call with empty ReasoningPart none stream 200
    tool call without ReasoningPart DeepSeek non-stream 200, key padded

Known limitation

Through a non-official base URL with no compat option set, padding only kicks in once the conversation already carries reasoning_content. Configure WithDeepSeekChatCompletionsCompat() for proxies. A runtime fallback (retry once on the specific 400) is deferred.

@Fodesu
Fodesu force-pushed the worktree-fix-completions-thinking-replay branch from d991950 to 4e04c51 Compare September 7, 2026 12:50
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
Fodesu force-pushed the worktree-fix-completions-thinking-replay branch from 4e04c51 to ffcced4 Compare September 7, 2026 12:56
@Fodesu
Fodesu marked this pull request as ready for review September 7, 2026 16:15
@Fodesu
Fodesu force-pushed the worktree-fix-completions-thinking-replay branch 2 times, most recently from 1866679 to ffcced4 Compare September 8, 2026 09:38
@Fodesu
Fodesu merged commit 18a9879 into felinics:main Sep 8, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant