Skip to content

Conversation

@simonrosenberg
Copy link
Collaborator

@simonrosenberg simonrosenberg commented Jan 22, 2026

Summary

This PR fixes issue #353 where DeepSeek requests fail with LLMBadRequestError: Missing \reasoning_content` field in the assistant message ... (400)`.

Problem

DeepSeek's thinking mode (deepseek-reasoner) requires the reasoning_content field to be present in assistant messages during tool call turns, even if it's None. The API returns a 400 error if this field is missing.

According to the DeepSeek documentation:

During the process of answering question 1 (Turn 1.1 - 1.3), the model performed multiple turns of thinking + tool calls before providing the answer. During this process, the user needs to send the reasoning content (reasoning_content) back to the API to allow the model to continue reasoning.

Solution

  1. Modified Message.to_chat_dict() to always include reasoning_content for assistant messages when send_reasoning_content is True, even if the value is None or empty string.

  2. Added deepseek-reasoner pattern to SEND_REASONING_CONTENT_MODELS to cover model names without provider prefix (e.g., deepseek-reasoner in addition to deepseek/deepseek-reasoner).

  3. Added example 35_deepseek_reasoning_content.py demonstrating how to use DeepSeek's reasoning mode with tool calls.

  4. Updated tests to reflect the new behavior where reasoning_content is always included for assistant messages when the feature is enabled.

Changes

  • openhands-sdk/openhands/sdk/llm/message.py: Modified to_chat_dict() to always include reasoning_content for assistant messages
  • openhands-sdk/openhands/sdk/llm/utils/model_features.py: Added deepseek-reasoner to SEND_REASONING_CONTENT_MODELS
  • examples/01_standalone_sdk/35_deepseek_reasoning_content.py: New example file
  • tests/sdk/llm/test_message.py: Updated tests for new behavior
  • tests/sdk/llm/test_model_features.py: Added test cases for DeepSeek model patterns
  • tests/sdk/llm/test_reasoning_content.py: Added DeepSeek-specific tests

Testing

All 498 tests in tests/sdk/llm/ pass, including the new tests for DeepSeek reasoning content behavior.

Fixes #353

@simonrosenberg can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:ef3a05f-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-ef3a05f-python \
  ghcr.io/openhands/agent-server:ef3a05f-python

All tags pushed for this build

ghcr.io/openhands/agent-server:ef3a05f-golang-amd64
ghcr.io/openhands/agent-server:ef3a05f-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:ef3a05f-golang-arm64
ghcr.io/openhands/agent-server:ef3a05f-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:ef3a05f-java-amd64
ghcr.io/openhands/agent-server:ef3a05f-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:ef3a05f-java-arm64
ghcr.io/openhands/agent-server:ef3a05f-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:ef3a05f-python-amd64
ghcr.io/openhands/agent-server:ef3a05f-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:ef3a05f-python-arm64
ghcr.io/openhands/agent-server:ef3a05f-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:ef3a05f-golang
ghcr.io/openhands/agent-server:ef3a05f-java
ghcr.io/openhands/agent-server:ef3a05f-python

About Multi-Architecture Support

  • Each variant tag (e.g., ef3a05f-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., ef3a05f-python-amd64) are also available if needed

DeepSeek's thinking mode requires the reasoning_content field to be present
in assistant messages during tool call turns, even if it's None. The API
returns a 400 error if this field is missing.

Changes:
- Modified Message.to_chat_dict() to always include reasoning_content for
  assistant messages when send_reasoning_content is True
- Added 'deepseek-reasoner' pattern to SEND_REASONING_CONTENT_MODELS to
  cover model names without provider prefix
- Added example 35_deepseek_reasoning_content.py demonstrating the feature
- Updated tests to reflect the new behavior

Fixes #353

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/llm
   message.py280897%370, 383–384, 392, 434, 530, 659–660
openhands-sdk/openhands/sdk/llm/utils
   model_features.py46197%32
TOTAL16113475270% 

Copy link
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix correctly addresses the DeepSeek reasoning_content requirement. Found a few minor issues to consider improving code clarity and maintainability.

message_dict["reasoning_content"] = self.reasoning_content
# Required for models like kimi-k2-thinking and deepseek-reasoner
# DeepSeek requires reasoning_content to be present in assistant messages
# during tool call turns, even if it's None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Important: The comment mentions "during tool call turns" which could be misinterpreted to mean that the code only includes reasoning_content for messages that have tool_calls. However, the implementation (lines 299-303) includes reasoning_content for ALL assistant messages when send_reasoning_content is True, regardless of whether tool calls are present.

Consider clarifying this comment to be more explicit, such as: "in all assistant messages when using reasoning models, even if it's None"

Comment on lines +299 to +303
if self.send_reasoning_content:
if self.role == "assistant":
# Always include reasoning_content for assistant messages when enabled
# Some providers (e.g., DeepSeek) require this field to be present
message_dict["reasoning_content"] = self.reasoning_content
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The nested conditionals could be simplified for better readability:

Suggested change
if self.send_reasoning_content:
if self.role == "assistant":
# Always include reasoning_content for assistant messages when enabled
# Some providers (e.g., DeepSeek) require this field to be present
message_dict["reasoning_content"] = self.reasoning_content
if self.send_reasoning_content and self.role == "assistant":
# Always include reasoning_content for assistant messages when enabled
# Some providers (e.g., DeepSeek) require this field to be present
message_dict["reasoning_content"] = self.reasoning_content

This makes the condition more explicit and easier to understand at a glance.

if self.role == "assistant":
# Always include reasoning_content for assistant messages when enabled
# Some providers (e.g., DeepSeek) require this field to be present
message_dict["reasoning_content"] = self.reasoning_content
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This PR introduces a behavior change that restricts reasoning_content to assistant messages only. Previously, reasoning_content could be included for any role if both send_reasoning_content and reasoning_content were truthy.

While this change makes sense (reasoning is produced by assistants, not users/tools/system), it would be helpful to document this behavior change in the PR description or add a note to the changelog for transparency. This helps users understand what changed and why, even if it's unlikely to affect existing code.

from openhands.tools.terminal import TerminalTool


logger = get_logger(__name__)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit: The logger is imported and created but never used in this example. While this appears to be a pattern in other example files for consistency, consider removing it if it's not needed.

(Or keep it if it's intentional for consistency with other examples - this is a very minor point.)

@openhands-ai
Copy link

openhands-ai bot commented Jan 22, 2026

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • [Optional] Docs example
    • Pre-commit checks

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1789 at branch `openhands/fix-deepseek-reasoning-content`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

Copy link
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous thinking about this:

#1464 (review)

I’m not fully sure what we may want to do here, and it may be worth digging a bit. Maybe the other version is unreachable today? 🤔

@simonrosenberg
Copy link
Collaborator Author

Previous thinking about this:

#1464 (review)

I’m not fully sure what we may want to do here, and it may be worth digging a bit. Maybe the other version is unreachable today? 🤔

This is a one-shot attempt with OpenHands...I should put it as draft

@simonrosenberg simonrosenberg marked this pull request as draft January 22, 2026 13:14
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.

5 participants