Skip to content

Preserve reasoning_content on assistant tool-call message in DeepSeek…#5908

Open
i-zoufeng wants to merge 1 commit intospring-projects:mainfrom
i-zoufeng:gh-5898-deepseek-reasoning-content-streaming
Open

Preserve reasoning_content on assistant tool-call message in DeepSeek…#5908
i-zoufeng wants to merge 1 commit intospring-projects:mainfrom
i-zoufeng:gh-5898-deepseek-reasoning-content-streaming

Conversation

@i-zoufeng
Copy link
Copy Markdown

… streaming

When deepseek-reasoner is used with streaming and tool calls, the API requires the assistant message replayed in subsequent rounds to include reasoning_content; otherwise it returns "thinking is enabled but reasoning_content is missing in assistant tool call message".

Spring AI dropped the field on three independent paths:

  1. DeepSeekStreamFunctionCallingHelper#merge did not accumulate the reasoning_content delta nor forward the prefix flag across chunks.

  2. DeepSeekChatModel#createRequest passed null for reasoning_content when re-serializing a DeepSeekAssistantMessage. Even after reading the typed field, Prompt#mutate() (called from buildRequestPrompt) downgrades AssistantMessage subclasses to the plain superclass via instructionsCopy, so subclass-only fields cannot survive a round trip; only metadata does.

  3. DeepSeekApi#chatCompletionStream emits each pre-tool-call chunk (reasoning delta, content delta) as its own one-element window for streaming UX and only merges chunks once the tool-call window opens. The chunk that triggers ToolCallingManager#executeToolCalls therefore carries only tool_calls, and the assistant message stored in the next round's conversation history has empty content and no reasoning_content.

Fixes:

  • Merge reasoning_content and forward prefix in the chunk-window merger.
  • Mirror reasoningContent and prefix from DeepSeekAssistantMessage into the message metadata, and read them back from metadata in createRequest, so the round trip works whether the message is a DeepSeekAssistantMessage or a plain AssistantMessage created by Prompt#instructionsCopy.
  • In DeepSeekChatModel#internalStream, maintain per-stream-call accumulators for content and reasoning_content. Before invoking executeToolCalls, enrich the assistant message with the accumulated values so the next round carries the full pre-tool-call context.

Adds regression tests covering chunk-merge accumulation, the AssistantMessage to ChatCompletionMessage round trip, and the streaming-path enrichment.

Fixes gh-5898

Thank you for taking time to contribute this pull request!
You might have already read the contributor guide, but as a reminder, please make sure to:

  • Add a Signed-off-by line to each commit (git commit -s) per the DCO
  • Rebase your changes on the latest main branch and squash your commits
  • Add/Update unit tests as needed
  • Run a build and make sure all tests pass prior to submission

For more details, please check the contributor guide.
Thank you upfront!

… streaming

When deepseek-reasoner is used with streaming and tool calls, the API
requires the assistant message replayed in subsequent rounds to include
reasoning_content; otherwise it returns "thinking is enabled but
reasoning_content is missing in assistant tool call message".

Spring AI dropped the field on three independent paths:

1. DeepSeekStreamFunctionCallingHelper#merge did not accumulate the
   reasoning_content delta nor forward the prefix flag across chunks.

2. DeepSeekChatModel#createRequest passed null for reasoning_content
   when re-serializing a DeepSeekAssistantMessage. Even after reading
   the typed field, Prompt#mutate() (called from buildRequestPrompt)
   downgrades AssistantMessage subclasses to the plain superclass via
   instructionsCopy, so subclass-only fields cannot survive a round
   trip; only metadata does.

3. DeepSeekApi#chatCompletionStream emits each pre-tool-call chunk
   (reasoning delta, content delta) as its own one-element window for
   streaming UX and only merges chunks once the tool-call window
   opens. The chunk that triggers ToolCallingManager#executeToolCalls
   therefore carries only tool_calls, and the assistant message stored
   in the next round's conversation history has empty content and no
   reasoning_content.

Fixes:

- Merge reasoning_content and forward prefix in the chunk-window merger.
- Mirror reasoningContent and prefix from DeepSeekAssistantMessage into
  the message metadata, and read them back from metadata in
  createRequest, so the round trip works whether the message is a
  DeepSeekAssistantMessage or a plain AssistantMessage created by
  Prompt#instructionsCopy.
- In DeepSeekChatModel#internalStream, maintain per-stream-call
  accumulators for content and reasoning_content. Before invoking
  executeToolCalls, enrich the assistant message with the accumulated
  values so the next round carries the full pre-tool-call context.

Adds regression tests covering chunk-merge accumulation, the
AssistantMessage to ChatCompletionMessage round trip, and the
streaming-path enrichment.

Fixes spring-projectsgh-5898

Signed-off-by: i-zoufeng <15536835114@163.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

Reasoning Content Missing Error in Stream API Calls with Tool Usage

1 participant