feat: Support reasoning content in Agent SDK#139
Conversation
Add support for intermediate thoughts from reasoning models like OpenAI o1, Anthropic Claude thinking, and DeepSeek R1 following Linus Torvalds' design principles. Key changes: - Extended Message class with reasoning_content and thinking_blocks fields - Added expose_reasoning config to LLM class (default: True) - Integrated reasoning extraction into completion() method flow - Reasoning content flows through events via Message.from_litellm_message() - Added comprehensive test suite with 14 tests covering all scenarios - Created usage examples for different reasoning providers Design follows 'good taste' principle - reasoning content as first-class citizen, not special case. LiteLLM standardizes reasoning_content across providers, thinking_blocks for Anthropic. Fixes #135 Co-authored-by: openhands <openhands@all-hands.dev>
xingyaoww
left a comment
There was a problem hiding this comment.
Also, we need to modify the .thought we extracted from openhands/sdk/event/llm_convertible.py for this format as well.
|
@OpenHands please address all the review comments in this PR |
- Move reasoning extraction logic from LLM class to Message.from_litellm_message (already implemented) - Remove expose_reasoning configuration option - always extract reasoning content - Remove reasoning_content_example.py file as requested - Add reasoning content fields to ActionEvent for event system integration - Update tests to reflect the changes Co-authored-by: openhands <openhands@all-hands.dev>
Summary: Successfully Addressed All PR Review CommentsI have successfully addressed all the review comments for PR #139 "feat: Support reasoning content in Agent SDK". Here's what was accomplished: ✅ Review Comments Addressed:
🎯 Key Improvements:
📝 Files Changed:
The changes have been committed and pushed to the |
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
@enyst in that case, shall we just keep |
|
@OpenHands let's just keep |
…content Since LiteLLM normalizes Anthropic thinking_blocks to reasoning_content, we only need the unified reasoning_content field. This simplifies the API and eliminates provider-specific handling. Changes: - Remove thinking_blocks field from Message class - Remove thinking_blocks field from ActionEvent class - Update from_litellm_message() to only extract reasoning_content - Update all tests to remove thinking_blocks references - Remove unused Any import from typing Co-authored-by: openhands <openhands@all-hands.dev>
Summary: Successfully Removed Anthropic-Specific
|
|
@enyst i poked it a bit but it looks like:
|
|
@enyst i think i got reasoning tokens working - but i can't get a model to get reasoning_content to show up :( |
GPT-5 is normal, needs Responses too. But the others, checking in the debugger now |
|
@enyst tried these models: they all didn't return that field :( |
…content\n\n- Adds examples/10_reasoning_debug.py\n- Defaults to deepseek-reasoner; configurable via REASONING_MODEL\n- Prints reasoning from event and llm_message; uses ConversationVisualizer\n\nCo-authored-by: openhands <openhands@all-hands.dev>
…dd_token_usage backward-compatible; add reasoning_tokens to expected dump in test\n\nCo-authored-by: openhands <openhands@all-hands.dev>
…tations with current reasoning support - Drop normalization case for DeepSeek-R1-0528 - Remove deepseek-r1-0528 from reasoning_effort params - Remove deepseek-r1-0528 from stop words related params Co-authored-by: openhands <openhands@all-hands.dev>
|
@xingyaoww Otherwise this is ready for review. It supports Gemini and Deepseek. I'd love to know what you think about the structure here, where we don't include |
…\nCo-authored-by: openhands <openhands@all-hands.dev>
…ages; errors are scaffold-only\n\n- Drop thought and reasoning_content fields from AgentErrorEvent schema\n- Stop passing thought/reasoning_content when emitting AgentErrorEvent\n- Update ConversationVisualizer to not display reasoning on AgentErrorEvent\n\nCo-authored-by: openhands <openhands@all-hands.dev>
…o preserve reasoning on proxy models like deepseek-reasoner\n\n- When mocking tool-calling via prompt, strip tools/tool_choice in outgoing request so providers/proxies don't downgrade models.\n- Validated with examples/10_reasoning_debug.py: DeepSeek Reasoner via LiteLLM proxy now returns reasoning_content and reasoning_tokens.\n\nCo-authored-by: openhands <openhands@all-hands.dev>
| has_tools_flag = ( | ||
| bool(tools) and use_native_fc | ||
| ) # only keep tools when native FC is active | ||
| call_kwargs = self._normalize_call_kwargs(kwargs, has_tools=has_tools_flag) |
There was a problem hiding this comment.
Note from the agent
- Before fix, the proxy was returning model=deepseek-chat whenever we sent the tools field to a model that doesn’t support native function calling. That dropped reasoning_content and reasoning tokens, so the probe showed NO with 0 tokens.
... which is confirmed 🫠
…local copy ignored\n\n- Deleted examples/10_reasoning_debug.py from git.\n- Added examples_local/ to .gitignore for dev-only copies.\n\nCo-authored-by: openhands <openhands@all-hands.dev>
xingyaoww
left a comment
There was a problem hiding this comment.
LGTM! Did some cleanup i think this is good to go!
Thank you for figuring things out!!

Summary
This PR implements support for intermediate thoughts from reasoning models like OpenAI o1, Anthropic Claude thinking, and DeepSeek R1 in the OpenHands Agent SDK.
Fixes #135
Key Changes
Message Class Extensions
reasoning_content: str | Nonefield for OpenAI o1 and DeepSeek R1 reasoningthinking_blocks: list[dict] | Nonefield for Anthropic Claude thinking blocksfrom_litellm_message()to extract reasoning content from LiteLLM responsesLLM Class Enhancements
expose_reasoning: bool = Trueconfiguration field_extract_reasoning_content()method to extract reasoning from ModelResponsecompletion()method flowexpose_reasoning=FalseEvent System Integration
Message.from_litellm_message()Design Philosophy
Following Linus Torvalds' "good taste" principle:
Provider Support
reasoning_contentreasoning_contentthinking_blocksUsage Examples
Enable Reasoning Content (Default)
Disable Reasoning Content
Anthropic Thinking Blocks
Testing
Files Changed
openhands/sdk/llm/message.py- Added reasoning fields and extraction logicopenhands/sdk/llm/llm.py- Added configuration and extraction methodtests/sdk/llm/test_reasoning_content.py- Comprehensive test suiteexamples/reasoning_content_example.py- Usage examples for all providersBackward Compatibility
✅ Fully backward compatible - existing code continues to work unchanged
✅ Optional fields - reasoning_content and thinking_blocks default to None
✅ Default behavior - reasoning content enabled by default for new users
✅ Configuration - users can disable if not needed
Technical Implementation
The implementation leverages LiteLLM's standardized reasoning content extraction:
getattr()for safe accessThis follows the principle of "good taste" - the solution eliminates special cases by treating reasoning content as a natural part of the message structure, not an afterthought.
@enyst can click here to continue refining the PR