Skip to content

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

Open
edelauna wants to merge 2 commits into
mainfrom
issue/201
Open

fix(deepseek): round-trip reasoning_content in thinking mode to prevent 400 errors#775
edelauna wants to merge 2 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 tool+reasoning scenarios).
    • Enhanced DeepSeek V4 end-to-end flows to retain and verify reasoning across chat-completions requests.
  • Bug Fixes

    • Fixed cases where assistant reasoning could be dropped or mishandled when reasoning was provided via structured content blocks and/or when tool calls were present.
  • Tests / Chores

    • Added regression and round-trip tests, and updated DeepSeek V4 fixtures to include the new reasoning field.

@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: fe742c35-5f12-4ccf-acf6-8bfb607e87d1

📥 Commits

Reviewing files that changed from the base of the PR and between 2c2f708 and 404744c.

📒 Files selected for processing (2)
  • apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts
  • src/api/transform/openai-format.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/vscode-e2e/src/suite/providers/deepseek-v4.test.ts
  • src/api/transform/openai-format.ts

📝 Walkthrough

Walkthrough

Adds reasoning_content support to convertToOpenAiMessages, including extraction from reasoning content 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 a "reasoning" content block when absent, 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
New tests validate 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 test capture logic computes and surfaces hasReasoningContentInHistory, and the reasoning-enabled and reasoning-disabled paths assert whether subsequent request history includes assistant reasoning_content.

Sequence Diagram(s)

sequenceDiagram
  participant AssistantMessage
  participant convertToOpenAiMessages
  participant OutgoingRequest

  AssistantMessage->>convertToOpenAiMessages: top-level reasoning_content or reasoning block
  convertToOpenAiMessages->>convertToOpenAiMessages: extract fallback reasoning text
  convertToOpenAiMessages->>OutgoingRequest: set reasoning_content before tool_calls
  OutgoingRequest->>OutgoingRequest: carry reasoning_content to next API call
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

Suggested reviewers

  • taltas
  • JamesRobert20
  • navedmerchant
  • hannesrudolph

Poem

A bunny found the missing thought,
And sent it back where it was sought. 🐇
Through tool calls too, it hops along,
With reasoning tucked inside the song.

🚥 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 The title clearly matches the main fix: preserving reasoning_content in DeepSeek thinking mode to avoid 400 errors.
Description check ✅ Passed The PR description includes the linked issue, change summary, implementation details, and a test procedure.
Linked Issues check ✅ Passed The changes address #201 by round-tripping reasoning_content into follow-up requests and adding regression coverage.
Out of Scope Changes check ✅ Passed The fixture and test updates are directly related to the DeepSeek reasoning_content fix and introduce no unrelated changes.
✨ 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 80.00000% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/api/transform/openai-format.ts 80.00% 0 Missing and 2 partials ⚠️

📢 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 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.

}
}
} 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.

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

2 participants