MoonshotAI / MoonshotAI/kimi-cli
fix(openai_legacy): ensure reasoning_content on ALL assistant messages for DeepSeek V4 compatibility
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 11.4k
- Forks
- 1.3k
- Avg merge
- 9h 47m
- Merged PRs (30d)
- 2
Description
Problem
When using DeepSeek V4 Pro with thinking mode enabled, multi-turn conversations with tool calls fail with a 400 error:
Error code: 400 - {'error': {'message': 'The `reasoning_content` in the thinking mode must be passed back to the API.'}}
Root Cause
DeepSeek V4 Pro API always returns reasoning_content in every assistant message when reasoning_effort is set — including plain text responses without tool calls. The API then requires reasoning_content to be present on all assistant messages in subsequent requests. If even one assistant message is missing reasoning_content, the API returns the 400 error above.
However, _convert_message() currently only adds reasoning_content when the message contains a ThinkPart:
# openai_legacy.py line 216
if reasoning_content and self._reasoning_key: # ← only adds when ThinkPart exists
dumped_message[self._reasoning_key] = reasoning_content
Plain text assistant responses (e.g., the final answer after a tool result) have no ThinkPart, so reasoning_content is not added to the dumped message. This violates DeepSeek V4's API contract.
Reproduction
- Enable thinking mode with DeepSeek V4 Pro
- Have a conversation flow: User → ToolCall → ToolResult → TextResponse → User → ToolCall → ToolResult
- The second request fails because the intermediate
TextResponseassistant message lacksreasoning_content
Minimal API-level reproduction
# ❌ Fails — text-only assistant without reasoning_content
messages = [
{"role": "user", "content": "weather?"},
{"role": "assistant", "content": "", "tool_calls": [...], "reasoning_content": "R1"},
{"role": "tool", ...},
{"role": "assistant", "content": "It's sunny."}, # ← MISSING reasoning_content!
{"role": "user", "content": "what about Busan?"},
{"role": "assistant", "content": "", "tool_calls": [...], "reasoning_content": "R2"},
{"role": "tool", ...}
]
# → 400: The reasoning_content in the thinking mode must be passed back to the API.
# ✅ Works — all assistant messages have reasoning_content
messages = [
...
{"role": "assistant", "content": "It's sunny.", "reasoning_content": "R_think"}, # ← OK
...
]
# → 200
Discovery & Verification
How this was found
- Export/import workaround: Running
/exportthen/importon a failing session resolves the issue. This works because export serializes all messages to markdown text, and import inserts it as a singleusermessage — eliminating all structured assistant messages and sidestepping thereasoning_contentrequirement. - Resume failure pattern: The error occurs on step 1 of a resumed session. The stored
context.jsonlhas assistant messages withThinkPart, but the intermediate text-only assistant messages have noThinkPart, soreasoning_contentis not added during conversion.
Fix verified locally
Tested by patching openai_legacy.py line 216.
The fix resolves the issue — DeepSeek V4 Pro thinking mode works correctly across multi-turn tool-call conversations.
Direct API verification (curl)
Confirmed via raw API calls that DeepSeek V4 Pro always returns reasoning_content when reasoning_effort is set, even for plain text responses:
# Simple "1+1=?" query with reasoning_effort="medium"
Response: {"role": "assistant", "content": "", "reasoning_content": "..."}
# Note: content is EMPTY — reasoning_content contains the answer!
And that having ALL assistant messages carry reasoning_content satisfies the API:
- ❌ Two tool-call rounds with a text-only assistant between them → 400
- ✅ Same messages but text-only assistant also has
reasoning_content→ 200
Request
When using DeepSeek models (or other backends that exhibit this behavior), ensure reasoning_content is added to all assistant messages in the history — not just those that originally had a ThinkPart. One possible approach:
- if reasoning_content and self._reasoning_key:
+ if self._reasoning_key and message.role == "assistant":
dumped_message[self._reasoning_key] = reasoning_content
PR #2053 added the reasoning_key default, but this condition also needs updating for DeepSeek compatibility. If a provider-agnostic change is undesirable, consider making it conditional on the provider's base URL or a new flag so only DeepSeek (and similarly behaving backends) are affected.
Scope
- DeepSeek V4 Pro: confirmed
- DeepSeek V4 Flash: same API behavior, likely affected
- File:
kosong/contrib/chat_provider/openai_legacy.py,_convert_message()method - Version: Kimi CLI 1.41.0
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in kosong/contrib/chat_provider/openai_legacy.py at _convert_message(), focusing on how assistant messages without a ThinkPart are converted. Reproduce the DeepSeek V4 multi-turn tool-call flow described in the issue and verify that all assistant messages preserve the required reasoning content without breaking other message conversions.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, cli
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100