Summary of the error
When using DeepSeek V4 Flash via BYOK (generic-chat-completion-api provider), the second and subsequent turns in a tool-call conversation fail with:
BYOK Error: 400 Error from provider (DeepSeek): The `reasoning_content` in the thinking mode must be passed back to the API.
DeepSeek V4 Flash with thinking mode enabled returns reasoning_content on assistant messages. When Droid replays the message history on the next API call, it strips this field. DeepSeek's API requires reasoning_content on every assistant message for tool-call turns, and rejects requests where it's missing.
Works: Single-turn queries, model responses without tool calls, models that don't generate reasoning_content (e.g. MiniMax, Kimi).
Fails: DeepSeek V4 Flash + tool calls + follow-up turn. V4 Pro also affected but intermittently.
Root Cause
In Droid's BYOK generic-chat-completion-api provider, when building the api_messages array for the next API request, the provider copies known fields (role, content, tool_calls) from stored messages but does not preserve reasoning_content. This is fine for most providers, but DeepSeek V4's thinking mode enforces a strict contract: reasoning_content from every assistant turn must be echoed back.
DeepSeek's own docs: https://api-docs.deepseek.com/guides/thinking_mode#tool-calls
"If your code does not correctly pass back reasoning_content, the API will return a 400 error."
Exact Fix (reference: Hermes Agent #16137, resolved)
Hermes Agent (NousResearch/hermes-agent) had this exact bug and fixed it with ~25 lines. The fix is in run_agent.py and consists of two parts:
Part 1: Detection (_needs_deepseek_tool_reasoning())
def _needs_deepseek_tool_reasoning(self) -> bool:
provider = (self.provider or "").lower()
model = (self.model or "").lower()
return (
provider == "deepseek"
or "deepseek" in model
or base_url_host_matches(self.base_url, "api.deepseek.com")
)
For Droid's case, this should also check for OpenCode Go's endpoint since that's the proxy used by many users:
// Droid equivalent
function needsDeepseekToolReasoning(provider: string, model: string, baseUrl: string): boolean {
return (
provider === "deepseek" ||
model.includes("deepseek") ||
baseUrl.includes("api.deepseek.com") ||
baseUrl.includes("opencode.ai/zen/go/v1") // OpenCode Go users
);
}
Part 2: Preservation in message-building loop
The call site (Hermes Agent line ~10397):
api_messages = []
for msg in messages:
api_msg = msg.copy()
self._copy_reasoning_content_for_api(msg, api_msg) # THIS IS THE KEY LINE
# ... strip internal fields ...
api_messages.append(api_msg)
Where _copy_reasoning_content_for_api (simplified):
def _copy_reasoning_content_for_api(self, source_msg, api_msg):
if source_msg.get("role") != "assistant":
return
existing = source_msg.get("reasoning_content")
if isinstance(existing, str):
# Preserve verbatim (empty string → space to avoid rejection)
api_msg["reasoning_content"] = existing or " "
Droid-specific fix location
In Droid's source, this needs to go wherever the BYOK generic-chat-completion-api provider builds the chat completion request body from stored messages. Likely in the buildRequestBody or convertToApiMessages function in the BYOK provider code path.
The fix is literally:
// In the message conversion loop, after copying msg to apiMsg:
+ if (needsDeepseekToolReasoning(provider, model, baseUrl) && msg.role === 'assistant' && msg.reasoning_content) {
+ apiMsg.reasoning_content = msg.reasoning_content;
+ }
Related Issues
Summary of the error
When using DeepSeek V4 Flash via BYOK (
generic-chat-completion-apiprovider), the second and subsequent turns in a tool-call conversation fail with:DeepSeek V4 Flash with thinking mode enabled returns
reasoning_contenton assistant messages. When Droid replays the message history on the next API call, it strips this field. DeepSeek's API requiresreasoning_contenton every assistant message for tool-call turns, and rejects requests where it's missing.Works: Single-turn queries, model responses without tool calls, models that don't generate reasoning_content (e.g. MiniMax, Kimi).
Fails: DeepSeek V4 Flash + tool calls + follow-up turn. V4 Pro also affected but intermittently.
Root Cause
In Droid's BYOK
generic-chat-completion-apiprovider, when building theapi_messagesarray for the next API request, the provider copies known fields (role, content, tool_calls) from stored messages but does not preservereasoning_content. This is fine for most providers, but DeepSeek V4's thinking mode enforces a strict contract:reasoning_contentfrom every assistant turn must be echoed back.DeepSeek's own docs: https://api-docs.deepseek.com/guides/thinking_mode#tool-calls
Exact Fix (reference: Hermes Agent #16137, resolved)
Hermes Agent (NousResearch/hermes-agent) had this exact bug and fixed it with ~25 lines. The fix is in
run_agent.pyand consists of two parts:Part 1: Detection (
_needs_deepseek_tool_reasoning())For Droid's case, this should also check for OpenCode Go's endpoint since that's the proxy used by many users:
Part 2: Preservation in message-building loop
The call site (Hermes Agent line ~10397):
Where
_copy_reasoning_content_for_api(simplified):Droid-specific fix location
In Droid's source, this needs to go wherever the BYOK
generic-chat-completion-apiprovider builds the chat completion request body from stored messages. Likely in thebuildRequestBodyorconvertToApiMessagesfunction in the BYOK provider code path.The fix is literally:
Related Issues