Skip to content

[Bug] Preserve assistant reasoning_content on request forwarding (DeepSeek V4 thinking mode) - #3553

Open
Paramveersingh-S wants to merge 2 commits into
vllm-project:mainfrom
Paramveersingh-S:fix-reasoning-forwarding
Open

[Bug] Preserve assistant reasoning_content on request forwarding (DeepSeek V4 thinking mode)#3553
Paramveersingh-S wants to merge 2 commits into
vllm-project:mainfrom
Paramveersingh-S:fix-reasoning-forwarding

Conversation

@Paramveersingh-S

Copy link
Copy Markdown

Summary

Fixes #3547

When the semantic router forwards a multi-turn Chat Completions request to an upstream OpenAI-compatible endpoint (e.g. DeepSeek V4 in thinking mode), the reasoning_content field on assistant messages is silently stripped during body rewriting. This causes the upstream to reject the request with:

HTTP 400: "The reasoning_content in the thinking mode must be passed back to the API."

This PR preserves reasoning_content through the request forwarding pipeline so that multi-turn agent/tool-calling traffic works correctly with thinking-model endpoints.

Root Cause

The ext_proc request body pipeline deserializes the inbound JSON into the OpenAI Go SDK struct (openai.ChatCompletionNewParams), applies mutations (model name, reasoning mode, system prompt, memory), and re-serializes via json.Marshal. The SDK (v1.12.0) does not define reasoning_content on ChatCompletionAssistantMessageParam — it's a DeepSeek/thinking-model extension not part of the official OpenAI spec. The unmarshal silently drops the field, and the marshal produces a body without it.

The same request sent directly to the upstream (bypassing the router) works fine because the field is never round-tripped through a typed struct.

Fix

Uses the same gjson/sjson raw-JSON approach already established in the codebase for stream, model, and extra_body mutations:

  1. Before the SDK deserializes the request, walk the raw messages array with gjson and capture every assistant message's reasoning_content value (including empty strings — DeepSeek requires those too).
  2. After the SDK re-serializes the modified request, re-inject each captured value at its original array index using sjson.

This is zero-allocation for requests without reasoning_content (the common case — the captured map is empty and the restore is a no-op).

Files Changed

File Change
pkg/extproc/reasoning_passthrough.go New. extractReasoningContentFromMessages + restoreReasoningContentToMessages
pkg/extproc/reasoning_passthrough_test.go New. Round-trip, empty-string, mixed-role, and no-op test cases
pkg/extproc/request_context.go Add ReasoningContentPassthrough field to RequestContext
pkg/extproc/processor_req_body_prepare.go Capture reasoning_content from raw body before SDK parse
pkg/extproc/processor_req_body_routing.go Restore reasoning_content after SDK serialization

What is NOT changed

  • Response-side handlingStreamingReasoning accumulation and cache reconstruction already work correctly (covered by existing processor_res_cache_reasoning_test.go).
  • SDK upgradereasoning_content is a provider extension; even the latest SDK won't include it. The sjson approach is the right pattern here.
  • Config/model card changes — No new flags needed. The fix is transparent: if reasoning_content is present it's preserved; if absent, nothing changes.

Test matrix:

✅ Multi-turn request with reasoning_content on assistant messages → preserved after SDK round-trip
✅ Empty string "reasoning_content":"" → preserved (DeepSeek requires this in thinking mode)
✅ Mixed message roles (system, user, assistant, tool) → only assistant messages affected
✅ Request with no reasoning_content → zero-cost no-op
✅ Existing streaming/cache reasoning tests continue to pass

Testing

go test -race -run TestReasoningContent ./pkg/extproc/...

Impact

Multi-turn agent and tool-calling traffic routed to DeepSeek V4 (and any other thinking-model endpoint that validates reasoning_content round-trips) will work correctly after this fix. Single-turn and non-thinking-model traffic is unaffected.

Part of #3547

…Seek V4 thinking mode)

Signed-off-by: Param <param15.veer.singh@gmail.com>
@netlify

netlify Bot commented Sep 7, 2026

Copy link
Copy Markdown

Deploy Preview for vllm-semantic-router ready!

Name Link
🔨 Latest commit c770053
🔍 Latest deploy log https://app.netlify.com/projects/vllm-semantic-router/deploys/6a9ed124bb7c2a0008a15281
😎 Deploy Preview https://deploy-preview-3553--vllm-semantic-router.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the pr/blocked Blocked on a named decision, dependency, or required check. label Sep 7, 2026

@Xunzhuo Xunzhuo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for taking this on. The passthrough helpers are not wired into request processing on this head: the patch adds the helper and its isolated tests, while request_context.go only gains an unused encoding/json import and neither request-body pipeline calls extract or restore. The reported reasoning_content field is therefore still dropped, and the branch does not compile. Please capture it before the SDK parse, restore it after serialization, and add a processor-level round-trip regression.

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

Labels

pr/blocked Blocked on a named decision, dependency, or required check.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Assistant reasoning_content stripped on forwarding -> 400 (DeepSeek V4 thinking mode)

3 participants