Skip to content

fix(deepseek): round-trip reasoning_content in thinking mode to prevent 400 errors#775

Merged
navedmerchant merged 5 commits into
mainfrom
issue/201
Jul 3, 2026
Merged

fix(deepseek): round-trip reasoning_content in thinking mode to prevent 400 errors#775
navedmerchant merged 5 commits into
mainfrom
issue/201

Conversation

@edelauna

@edelauna edelauna commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Related GitHub Issue

Closes: #201

Description

When DeepSeek thinking mode is active, the API requires reasoning_content to be passed back on every assistant message in conversation history. Without it, the API returns HTTP 400: The reasoning_content in the thinking mode must be passed back to the API.

Root cause — two bugs, one symptom:

  1. convertToOpenAiMessages (used by the generic OpenAI-compatible provider path) silently dropped reasoning content blocks and never set reasoning_content on outgoing messages. This is the primary fix.

  2. aimock fixture format — the e2e fixtures had reasoning + toolCalls but no content, so aimock classified them as ToolCallResponse (which ignores reasoning) instead of ContentWithToolCallsResponse. Fixed by adding "content": "".

What changed:

  • src/api/transform/openai-format.tsconvertToOpenAiMessages now extracts reasoning_content from both the top-level field (already round-tripped) and { type: "reasoning" } content blocks (stored by Task.ts during streaming), and sets it on the outgoing assistant message.
  • src/api/transform/__tests__/openai-format.spec.ts — 5 new unit tests covering the round-trip: string-content path, array-content path, array-content with tool calls (the exact failure case), preference ordering, and absent-reasoning no-op.
  • src/api/providers/__tests__/openai.spec.ts — provider-level integration test: OpenAiHandler.createMessage with preserveReasoning: true and an assistant message containing a reasoning block + tool call; asserts reasoning_content appears in the body sent to the OpenAI client. This directly exercises the fixed OpenAiHandler → convertToOpenAiMessages path.
  • apps/vscode-e2e/fixtures/deepseek-v4.json — added "content": "" to reasoning-on fixtures so aimock routes them through buildContentWithToolCallsChunks and streams reasoning_content in SSE deltas.
  • apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts — e2e regression guard for the native DeepSeek provider path: asserts hasReasoningContentInHistory on the second request (after the tool call), proving reasoning_content from turn 1 survives into turn 2's history.

Coverage note: The native DeepSeekHandler → convertToR1Format path was already correct before this PR. The e2e test guards that path. The fix to convertToOpenAiMessages is guarded by unit tests + the new provider-level integration test in openai.spec.ts.

Test Procedure

Unit tests:

pnpm test

All 408 test files pass (6,584 tests).

E2e tests (mock):

TEST_FILE=deepseek-v4.test pnpm --filter @roo-code/vscode-e2e test:ci:mock

All 4 DeepSeek V4 provider tests pass, including the reasoning-on probes that assert hasReasoningContentInHistory on the second request.

Live API validation (performed during development):

  • Confirmed DeepSeek returns content: "" + reasoning_content: "..." + tool_calls: [...] on turn 1 tool-call responses.
  • Confirmed turn 2 with reasoning_content round-tripped returns HTTP 200.

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I have performed a thorough self-review of my code.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Documentation Impact: No documentation updates required (internal API behaviour fix).
  • Contribution Guidelines: I have read and agree to the Contributor Guidelines.

Summary by CodeRabbit

  • New Features

    • Added preservation and round-trip support for assistant reasoning_content in OpenAI-compatible message formatting, including DeepSeek/Z.ai thinking mode and mixed tool+reasoning turns.
    • Expanded DeepSeek V4 end-to-end validation to detect and verify reasoning presence across chat-completions requests.
  • Bug Fixes

    • Fixed scenarios where assistant reasoning could be omitted or mis-mapped when provided via structured content blocks and when tool calls were included.
  • Tests / Chores

    • Added regression and format round-trip tests, updated DeepSeek V4 fixtures, and strengthened E2E assertions/diagnostics for reasoning history.

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bf1dc552-7200-4b18-a4ed-fb0970ad4410

📥 Commits

Reviewing files that changed from the base of the PR and between fe7f41a and 6b5b6f1.

📒 Files selected for processing (1)
  • src/api/transform/__tests__/openai-format.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api/transform/tests/openai-format.spec.ts

📝 Walkthrough

Walkthrough

Adds reasoning_content support to OpenAI message conversion, including extraction from reasoning blocks and preservation on outgoing assistant messages before tool_calls. Updates unit tests, DeepSeek fixtures, and e2e capture logic to verify the field round-trips into subsequent requests.

Changes

reasoning_content round-trip

Layer / File(s) Summary
reasoning_content extraction and assignment in convertToOpenAiMessages
src/api/transform/openai-format.ts
Adds optional reasoning_content fields to OpenAI message shapes, copies top-level reasoning_content for string-content messages, extracts reasoning text from reasoning content blocks when present, and sets the outgoing field before tool_calls are attached.
Unit tests validating reasoning_content conversion
src/api/transform/__tests__/openai-format.spec.ts, src/api/providers/__tests__/openai.spec.ts
Tests cover reasoning_content passthrough, block extraction, precedence, coexistence with tool_use, undefined fallback, and serialization via OpenAiHandler when preserveReasoning is set.
DeepSeek e2e fixtures and request capture for reasoning_content
apps/vscode-e2e/fixtures/deepseek-v4.json, apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts
Fixture responses gain a reasoning field; e2e capture logic computes hasReasoningContentInHistory, and the reasoning-enabled and reasoning-disabled paths assert whether subsequent request history includes assistant reasoning_content.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested labels: awaiting-review

Suggested reviewers: taltas, JamesRobert20, navedmerchant, hannesrudolph

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed It clearly summarizes the main fix: round-tripping reasoning_content in DeepSeek thinking mode to avoid 400 errors.
Description check ✅ Passed The PR includes the required issue link, change summary, test procedure, and checklist; optional sections are acceptable.
Linked Issues check ✅ Passed The code preserves reasoning_content through conversion and adds tests/fixtures to ensure follow-up DeepSeek requests include it, matching #201.
Out of Scope Changes check ✅ Passed All changes support the DeepSeek reasoning_content fix or its regression tests; no unrelated functionality appears added.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue/201

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@edelauna edelauna changed the title fix(deepseek): reasoning_content handler across deepseek model providers fix(deepseek): round-trip reasoning_content in thinking mode to prevent 400 errors Jun 30, 2026
@codecov

codecov Bot commented Jun 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.44444% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/api/transform/openai-format.ts 94.44% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@edelauna edelauna marked this pull request as ready for review June 30, 2026 22:58

@coderabbitai coderabbitai Bot 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/transform/openai-format.ts (1)

462-481: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Multiple reasoning blocks in one turn will silently drop earlier reasoning text.

extractedReasoning is overwritten on every matching "reasoning" block rather than accumulated, so only the last block's text survives. Since the PR explicitly targets DeepSeek/Z.ai interleaved thinking (reasoning interspersed with tool calls in the same turn), a turn with more than one reasoning segment will lose all but the final one when round-tripped back to the API.

♻️ Proposed fix: accumulate instead of overwrite
-				let extractedReasoning: string | undefined
+				const extractedReasoningParts: string[] = []
 				const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
 					nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
 					toolMessages: Anthropic.ToolUseBlockParam[]
 				}>(
 					(acc, part) => {
 						if (part.type === "tool_use") {
 							acc.toolMessages.push(part)
 						} else if (part.type === "text" || part.type === "image") {
 							acc.nonToolMessages.push(part)
 						} else if ((part as any).type === "reasoning" && (part as any).text) {
-							// Extract reasoning stored as a content block (DeepSeek / Z.ai interleaved thinking).
-							// Must be passed back as top-level reasoning_content so providers like DeepSeek
-							// don't reject the request with "reasoning_content must be passed back to the API".
-							extractedReasoning = (part as any).text
+							extractedReasoningParts.push((part as any).text)
 						} // assistant cannot send tool_result messages
 						return acc
 					},
 					{ nonToolMessages: [], toolMessages: [] },
 				)
+				const extractedReasoning = extractedReasoningParts.length > 0 ? extractedReasoningParts.join("\n") : undefined
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/transform/openai-format.ts` around lines 462 - 481, In
openai-format.ts, the reasoning extraction in the anthropicMessage.content
reduce currently overwrites extractedReasoning on each "reasoning" block, so
earlier segments are lost. Update the reducer logic in the
nonToolMessages/toolMessages extraction path to accumulate all reasoning text
blocks (preserving order) instead of replacing the previous value, and ensure
the final reasoning_content passed back for DeepSeek/Z.ai interleaved thinking
includes every reasoning block found in the turn.
🧹 Nitpick comments (1)
apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts (1)

382-395: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Solid regression assertion for the round-trip fix.

Existence check on secondRequest before asserting on hasReasoningContentInHistory avoids a brittle false-positive/undefined failure. Consider also asserting hasReasoningContentInHistory is false on the corresponding reasoning-off probe's second request, to guard against the flag becoming a tautology (e.g., always true due to a capture bug).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts` around lines 382 -
395, The regression check in deepseek-v4.test.ts should not only verify that the
reasoning-enabled flow preserves reasoning_content on the second request via
secondRequest.hasReasoningContentInHistory, but also guard against a capture bug
by asserting the equivalent second request in the reasoning-off probe keeps that
flag false. Use the existing probe/request objects in the DeepSeek test suite to
add the negative assertion alongside the current positive one so the flag is
validated in both modes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/api/transform/openai-format.ts`:
- Around line 462-481: In openai-format.ts, the reasoning extraction in the
anthropicMessage.content reduce currently overwrites extractedReasoning on each
"reasoning" block, so earlier segments are lost. Update the reducer logic in the
nonToolMessages/toolMessages extraction path to accumulate all reasoning text
blocks (preserving order) instead of replacing the previous value, and ensure
the final reasoning_content passed back for DeepSeek/Z.ai interleaved thinking
includes every reasoning block found in the turn.

---

Nitpick comments:
In `@apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts`:
- Around line 382-395: The regression check in deepseek-v4.test.ts should not
only verify that the reasoning-enabled flow preserves reasoning_content on the
second request via secondRequest.hasReasoningContentInHistory, but also guard
against a capture bug by asserting the equivalent second request in the
reasoning-off probe keeps that flag false. Use the existing probe/request
objects in the DeepSeek test suite to add the negative assertion alongside the
current positive one so the flag is validated in both modes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9edf7d50-fd91-4a2f-9e82-45ae5020c01e

📥 Commits

Reviewing files that changed from the base of the PR and between 67df9f9 and 2c2f708.

📒 Files selected for processing (5)
  • apps/vscode-e2e/fixtures/deepseek-v4.json
  • apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts
  • src/api/providers/__tests__/openai.spec.ts
  • src/api/transform/__tests__/openai-format.spec.ts
  • src/api/transform/openai-format.ts

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jun 30, 2026
@github-actions github-actions Bot added awaiting-review PR changes are ready and waiting for maintainer re-review and removed awaiting-review PR changes are ready and waiting for maintainer re-review labels Jul 1, 2026
taltas
taltas previously approved these changes Jul 1, 2026

@taltas taltas 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.

Clean PR, except one question with the typing.

Comment thread src/api/transform/openai-format.ts Outdated
}
}
} else if (anthropicMessage.role === "assistant") {
const messageWithDetails = anthropicMessage as any

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.

I am not a big fan of this any here... maybe we should better define the type here? It's not really a blocker, but something to consider.

@github-actions github-actions Bot removed the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 3, 2026
Add a regression test that passes a non-object content part (a stray
string) through convertToOpenAiMessages, exercising the defensive
 early-return in getReasoningBlockText
(line 276). This was the only uncovered line in the PR patch, causing the
codecov/patch check to fail.
@navedmerchant navedmerchant added this pull request to the merge queue Jul 3, 2026
@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 3, 2026
Merged via the queue into main with commit 8e76b8d Jul 3, 2026
14 checks passed
@navedmerchant navedmerchant deleted the issue/201 branch July 3, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] DeepSeek error: 'reasoning_content' must be passed back to the API

3 participants