Skip to content

fix(transport): emit deepseek-v4 thinking.type and reasoning_effort on non-OpenRouter routes - #16614

Closed
Skyline10124 wants to merge 1 commit into
NousResearch:mainfrom
Skyline10124:fix/deepseek-v4-thinking-mode
Closed

fix(transport): emit deepseek-v4 thinking.type and reasoning_effort on non-OpenRouter routes#16614
Skyline10124 wants to merge 1 commit into
NousResearch:mainfrom
Skyline10124:fix/deepseek-v4-thinking-mode

Conversation

@Skyline10124

Copy link
Copy Markdown

What does this PR do?

Add DeepSeek V4 thinking mode support that works through any route (direct api.deepseek.com, opencode-go, or other OpenAI-compatible relays), not just OpenRouter.

Currently _supports_reasoning_extra_body() gates on base_url containing "openrouter", which silently drops reasoning_config for direct DeepSeek connections and opencode-go. This PR makes reasoning_effort and thinking.type actually reach the API.

Related Issue

Fixes #15717

Related to #14958, #15251 — those PRs correct the detection gate but stop at run_agent.py. This PR completes the pipeline end-to-end.

Type of Change

  • 🐛 Bug fix (reasoning_effort was silently dropped for non-OpenRouter routes)
  • ✨ New feature (DeepSeek V4 thinking mode via any route)

Changes Made

run_agent.py_supports_reasoning_extra_body() (1 line)

  • Detect DeepSeek V4 models by name prefix (deepseek-v4), so routes like opencode-go (opencode.ai/zen/go/v1) and any future relay can enable thinking.

agent/transports/chat_completions.pybuild_kwargs() (28 lines)

  • New is_deepseek_v4 branch, modelled after the existing is_kimi handling
  • Top-level reasoning_effort with effort mapping: xhighmax, others → high
  • extra_body["thinking"] = {"type": "enabled"/"disabled"} — DeepSeek-native format
  • Exclude DeepSeek V4 from the generic extra_body["reasoning"] path to avoid format conflict

tests/agent/transports/test_chat_completions.py — 7 new tests

  • reasoning_effort top-level (high, xhigh→max, disabled→omitted)
  • extra_body.thinking enabled/disabled
  • No stray extra_body.reasoning for V4
  • deepseek-chat (V2/V3) untouched — regression guard

How to Test

  1. Configure a DeepSeek V4 model without OpenRouter (e.g. provider: opencode-go, base_url: https://opencode.ai/zen/go/v1, reasoning_effort: xhigh)
  2. Start a session and ask a reasoning-intensive question
  3. Verify the API request includes reasoning_effort: "max" and extra_body: {"thinking": {"type": "enabled"}}

Or run the transport-level unit tests:

python -m pytest tests/agent/transports/test_chat_completions.py -k "deepseek" -v

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation — or N/A
  • I've updated cli-config.yaml.example — or N/A
  • I've considered cross-platform impact — or N/A

Why this approach

Existing PRs (#14958, #15251) add is_deepseek detection in run_agent.py but don't touch build_kwargs() — so the transport still emits OpenRouter-style extra_body["reasoning"] format. #15577 introduces a unified thinking_mode parameter framework.

This PR takes the minimal path:

  • No new config parameter — reuses the existing reasoning_config flow
  • Model-name-based gating — works for opencode-go, direct API, and any future relay
  • Scoped to V4 onlydeepseek-v4* prefix match; deepseek-chat / deepseek-reasoner (legacy) are untouched
  • Follows existing patterns — the code structure mirrors is_kimi handling, keeping the transport predictable

Copilot AI review requested due to automatic review settings April 27, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Extends the agent → transport plumbing so DeepSeek V4 “thinking mode” parameters (reasoning_effort and extra_body.thinking.type) are emitted based on the model name (e.g., deepseek-v4-*), enabling non-OpenRouter routes (direct DeepSeek API, OpenAI-compatible relays) to receive the correct request shape.

Changes:

  • Update run_agent.py reasoning-extra-body gating to treat deepseek-v4* as reasoning-capable.
  • Update ChatCompletionsTransport.build_kwargs() to emit DeepSeek V4’s native reasoning_effort + extra_body.thinking and to avoid the generic extra_body["reasoning"] path for V4.
  • Add transport-level unit tests covering DeepSeek V4 request shaping and guarding legacy deepseek-chat behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/agent/transports/test_chat_completions.py Adds DeepSeek V4-focused unit tests for reasoning_effort and extra_body.thinking behavior.
run_agent.py Adjusts _supports_reasoning_extra_body() to return true for deepseek-v4* models.
agent/transports/chat_completions.py Adds DeepSeek V4 request-shaping branch and excludes V4 from generic extra_body.reasoning.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread run_agent.py
Comment on lines +7857 to +7860
# DeepSeek V4 series: supports reasoning_effort + thinking via any route
if self.model and self.model.lower().startswith("deepseek-v4"):
return True

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

_supports_reasoning_extra_body() is also used in _handle_max_iterations() to decide whether to send extra_body["reasoning"] (OpenRouter-style). Returning True for deepseek-v4* will make iteration-limit summary requests include extra_body.reasoning, which conflicts with the DeepSeek V4 contract (should use top-level reasoning_effort + extra_body.thinking). Suggest keeping this method scoped to the generic extra_body.reasoning format and instead handle DeepSeek V4 summary kwargs via the chat_completions transport (or add an explicit DeepSeek V4 branch in the summary path).

Copilot uses AI. Check for mistakes.
Comment thread agent/transports/chat_completions.py Outdated
@@ -188,6 +188,7 @@ def build_kwargs(
anthropic_max_out = params.get("anthropic_max_output")
is_nvidia_nim = params.get("is_nvidia_nim", False)
is_kimi = params.get("is_kimi", False)
is_deepseek_v4 = params.get("model_lower", "").startswith("deepseek-v4")

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

DeepSeek V4 detection uses params.get("model_lower", ""), which means the DeepSeek V4 branch silently won’t run if a caller relies on the existing fallback model_lower = params.get("model_lower", (model or "").lower()) (i.e., doesn’t pass model_lower). Use the already-computed local model_lower (or (model or "").lower()) to keep detection consistent across call sites.

Suggested change
is_deepseek_v4 = params.get("model_lower", "").startswith("deepseek-v4")
is_deepseek_v4 = model_lower.startswith("deepseek-v4")

Copilot uses AI. Check for mistakes.
Comment on lines 224 to +268
@@ -240,8 +256,19 @@ def build_kwargs(
"type": "enabled" if _kimi_thinking_enabled else "disabled",
}

# DeepSeek V4: extra_body.thinking
if is_deepseek_v4:
_ds_thinking_off = bool(
reasoning_config
and isinstance(reasoning_config, dict)
and reasoning_config.get("enabled") is False
)
extra_body["thinking"] = {
"type": "enabled" if not _ds_thinking_off else "disabled",
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The DeepSeek V4 block repeats the same _ds_thinking_off computation in two places (reasoning_effort + extra_body.thinking). Consider computing it once and reusing it to reduce duplication and the chance of future drift between the two branches.

Copilot uses AI. Check for mistakes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread agent/transports/chat_completions.py Outdated
_ds_thinking_off = bool(
reasoning_config
and isinstance(reasoning_config, dict)
and reasoning_config.get("enabled") is False

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

_ds_thinking_off only checks reasoning_config['enabled'] is False, but other parts of the codebase pass reasoning_config={"effort": "none"} to disable thinking (e.g., batch_runner's --reasoning_disabled). For DeepSeek V4 this currently still sends reasoning_effort and sets extra_body.thinking.type to enabled. Consider treating effort == "none" (case/whitespace-normalized) as thinking disabled here as well, and omit reasoning_effort in that case.

Suggested change
and reasoning_config.get("enabled") is False
and (
reasoning_config.get("enabled") is False
or (reasoning_config.get("effort") or "").strip().lower() == "none"
)

Copilot uses AI. Check for mistakes.
Comment thread run_agent.py
Comment on lines +9356 to 9363
_ds_thinking_off = (
self.reasoning_config is not None
and isinstance(self.reasoning_config, dict)
and self.reasoning_config.get("enabled") is False
)
summary_extra_body["thinking"] = {
"type": "enabled" if not _ds_thinking_off else "disabled",
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

DeepSeek V4 summary requests treat thinking as disabled only when reasoning_config.enabled is False, but callers may also disable thinking via reasoning_config={"effort": "none"} (used elsewhere in the codebase). With the current check, V4 summary calls will still set extra_body.thinking.type to enabled even when the user requested no thinking. Update _ds_thinking_off to also consider effort == "none" as disabled (normalize case/whitespace).

Copilot uses AI. Check for mistakes.
Comment on lines +266 to +290
def test_deepseek_v4_reasoning_effort_omitted_when_thinking_disabled(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro", messages=[{"role": "user", "content": "Hi"}],
model_lower="deepseek-v4-pro",
reasoning_config={"enabled": False},
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert "reasoning_effort" not in kw

def test_deepseek_v4_thinking_enabled_extra_body(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro", messages=[{"role": "user", "content": "Hi"}],
model_lower="deepseek-v4-pro",
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["extra_body"]["thinking"] == {"type": "enabled"}

def test_deepseek_v4_thinking_disabled_extra_body(self, transport):
kw = transport.build_kwargs(
model="deepseek-v4-pro", messages=[{"role": "user", "content": "Hi"}],
model_lower="deepseek-v4-pro",
reasoning_config={"enabled": False},
max_tokens_param_fn=lambda n: {"max_tokens": n},
)
assert kw["extra_body"]["thinking"] == {"type": "disabled"}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

The new DeepSeek V4 tests cover reasoning_config={"enabled": False} but not the existing disable shape reasoning_config={"effort": "none"} (used by CLI/batch runner paths). Add a regression test asserting that effort=none results in extra_body.thinking.type == "disabled" and that reasoning_effort is omitted for V4.

Copilot uses AI. Check for mistakes.
- _supports_reasoning_extra_body(): detect deepseek-v4 models by name
- build_kwargs(): emit reasoning_effort top-level (xhigh→max, others→high)
  and extra_body.thinking.type=enabled/disabled for DeepSeek V4
- Exclude DeepSeek V4 from generic extra_body.reasoning path
- Add 7 transport-level tests covering enabled/disabled/xhigh/chat-clean
@Skyline10124
Skyline10124 force-pushed the fix/deepseek-v4-thinking-mode branch from feeb834 to 7d329ae Compare April 27, 2026 15:56
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/deepseek DeepSeek API labels Apr 27, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #14958 — same fix: plumb reasoning_effort and thinking toggle to DeepSeek V4 API for non-OpenRouter routes. Multiple competing PRs exist (#15577, #16448, #15251).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/deepseek DeepSeek API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: DeepSeek API 400 error: "reasoning_content" in thinking mode must be passed back to the API

3 participants