Skip to content

Droid BYOK drops reasoning_content for DeepSeek V4 Flash (tool-call turns → 400 error) #1091

@RealNarcissus

Description

@RealNarcissus

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions