Summary
When using deepseek-v4-pro with thinking/reasoning mode (the default), every request after the first one that involves tool use fails with:
The content[].thinking in the thinking mode must be passed back to the API.
This is 100% reproducible — either on the very first thinking-mode turn of a fresh conversation, or immediately after Codex compacts/rewrites its context window mid-session. Once it starts, every retry fails identically and the conversation never recovers without starting a new session.
I traced this to root cause in the source and believe I've found the exact gap: the code to fix this already exists, is unit-tested, and is simply never called.
Reproduction
Minimal repro — fresh conversation, req_messages=5:
15:25:49.891 INFO 流式请求完成 model=deepseek-v4-pro ... output_tokens=533 duration=7.87s ← turn 1 succeeds
15:25:50.791 ERROR 提供商错误 status=400 error="The content[].thinking..." req_messages=5 ← turn 2 fails immediately, then loops forever
Also reproducible mid-session, immediately after Codex compacts context (cache hit rate collapses from ~100% to 5.7%, input_total drops sharply — evidence of history rebuild):
15:15:02 input_total=243428 cache_hit_rate~100%
15:17:44 input_total=183257 input_fresh=172761 cache_hit_rate=5.7% ← compaction event
15:17:50 input_total=20878 duration=5.5s ← succeeds
15:17:51 status=400 error="thinking..." req_messages=22 ← fails immediately, loops forever
Disabling all reasoning-related config on the model (default_reasoning_level, supported_reasoning_levels, supports_reasoning_summaries, default_reasoning_summary) has no effect — identical failure. This confirms these are client-facing capability-advertisement fields only, not something that stops DeepSeek V4 Pro from reasoning by default. The fix is not available at the config level
Root cause trace
- internal/extension/plugin/capabilities.go:114-117 defines a ThinkingPrepender interface:
go
type ThinkingPrepender interface {
PrependThinkingForToolUse(messages []format.CoreMessage, toolCallID string, pendingSummary []openai.ReasoningItemSummary, sessionState any) []format.CoreMessage
PrependThinkingForAssistant(blocks []format.CoreContentBlock, pendingSummary []openai.ReasoningItemSummary, sessionState any) []format.CoreContentBlock
}
-
internal/extension/deepseek_v4/plugin.go:190-223 — DSPlugin fully implements both methods, with real logic: check for a pending reasoning summary from the OpenAI Responses payload, fall back to per-tool-call/per-text cached thinking blocks in session state (state.go), and finally fall back to an empty placeholder (RequiredThinkingBlock()) with a warning log. This is solid, deliberate implementation — and has dedicated unit tests in deepseek_v4_test.go.
-
internal/extension/plugin/registry.go:24-35 — the Registry struct maintains typed slices for every dispatchable capability (errorTransformers, sessionProviders, logConsumers, etc.) — but there is no thinkingPrependers []ThinkingPrepender field.
-
internal/extension/plugin/registry.go:94-107 — the plugin-registration block does if v, ok := p.(X); ok { r.xSlice = append(...) } for every other capability interface — there is no equivalent check for ThinkingPrepender. A plugin implementing it is silently never collected.
-
internal/format/adapter.go:194-196 — separately, CorePluginHooks defines a PrependThinkingToAssistant hook slot, matching the naming/shape of other hooks like MutateCoreRequest.
-
internal/format/adapter.go:244-246 — WithDefaults() gives it a no-op default, same pattern as every other hook.
-
Confirmed via full-repo search — hooks.PrependThinkingToAssistant( is never called anywhere in the codebase. Compare to its sibling hook MutateCoreRequest, which is correctly called from four separate protocol adapters at the point outbound requests are built:
internal/protocol/anthropic/adapter.go:262
internal/protocol/chat/adapter.go:68
internal/protocol/google/adapter.go:85
internal/protocol/openai/adapter.go:180
PrependThinkingToAssistant has zero call sites anywhere in the pipeline.
Conclusion
The DeepSeek V4 thinking round-trip logic is fully implemented, tested, and even has a dedicated hook slot reserved for it in the core plugin architecture — but it's disconnected at two separate points:
Registry never collects plugins implementing ThinkingPrepender (no slice, no registration check).
Even if it did, no protocol adapter calls hooks.PrependThinkingToAssistant(...), unlike its sibling MutateCoreRequest, which is wired correctly in 4 places.
Suggested fix
Mirror the existing TransformError/errorTransformers pattern for ThinkingPrepender:
Add thinkingPrependers []ThinkingPrepender to Registry (registry.go ~line 30).
Register it alongside the other capability checks (registry.go ~line 98-107):
go
if v, ok := p.(ThinkingPrepender); ok {
r.thinkingPrependers = append(r.thinkingPrependers, v)
}
Add a dispatch method on Registry, mirroring TransformError's shape, that calls PrependThinkingForAssistant/PrependThinkingForToolUse for each registered plugin enabled for the current model.
Wire hooks.PrependThinkingToAssistant (wherever CorePluginHooks gets constructed per-request/per-model) to call the new Registry dispatch method.
Call a.hooks.PrependThinkingToAssistant(ctx, req) in the same protocol adapters where MutateCoreRequest is already called — at minimum internal/protocol/openai/adapter.go (used by Codex) and internal/protocol/anthropic/adapter.go.
Given the DeepSeek plugin's side of this is already correct and tested, this looks like a contained fix — connecting existing, working pieces rather than writing new logic.
Summary
When using deepseek-v4-pro with thinking/reasoning mode (the default), every request after the first one that involves tool use fails with:
The
content[].thinkingin the thinking mode must be passed back to the API.This is 100% reproducible — either on the very first thinking-mode turn of a fresh conversation, or immediately after Codex compacts/rewrites its context window mid-session. Once it starts, every retry fails identically and the conversation never recovers without starting a new session.
I traced this to root cause in the source and believe I've found the exact gap: the code to fix this already exists, is unit-tested, and is simply never called.
Reproduction
Minimal repro — fresh conversation, req_messages=5:
15:25:49.891 INFO 流式请求完成 model=deepseek-v4-pro ... output_tokens=533 duration=7.87s ← turn 1 succeeds
15:25:50.791 ERROR 提供商错误 status=400 error="The
content[].thinking..." req_messages=5 ← turn 2 fails immediately, then loops foreverAlso reproducible mid-session, immediately after Codex compacts context (cache hit rate collapses from ~100% to 5.7%, input_total drops sharply — evidence of history rebuild):
15:15:02 input_total=243428 cache_hit_rate~100%
15:17:44 input_total=183257 input_fresh=172761 cache_hit_rate=5.7% ← compaction event
15:17:50 input_total=20878 duration=5.5s ← succeeds
15:17:51 status=400 error="thinking..." req_messages=22 ← fails immediately, loops forever
Disabling all reasoning-related config on the model (default_reasoning_level, supported_reasoning_levels, supports_reasoning_summaries, default_reasoning_summary) has no effect — identical failure. This confirms these are client-facing capability-advertisement fields only, not something that stops DeepSeek V4 Pro from reasoning by default. The fix is not available at the config level
Root cause trace
go
type ThinkingPrepender interface {
PrependThinkingForToolUse(messages []format.CoreMessage, toolCallID string, pendingSummary []openai.ReasoningItemSummary, sessionState any) []format.CoreMessage
PrependThinkingForAssistant(blocks []format.CoreContentBlock, pendingSummary []openai.ReasoningItemSummary, sessionState any) []format.CoreContentBlock
}
internal/extension/deepseek_v4/plugin.go:190-223 — DSPlugin fully implements both methods, with real logic: check for a pending reasoning summary from the OpenAI Responses payload, fall back to per-tool-call/per-text cached thinking blocks in session state (state.go), and finally fall back to an empty placeholder (RequiredThinkingBlock()) with a warning log. This is solid, deliberate implementation — and has dedicated unit tests in deepseek_v4_test.go.
internal/extension/plugin/registry.go:24-35 — the Registry struct maintains typed slices for every dispatchable capability (errorTransformers, sessionProviders, logConsumers, etc.) — but there is no thinkingPrependers []ThinkingPrepender field.
internal/extension/plugin/registry.go:94-107 — the plugin-registration block does if v, ok := p.(X); ok { r.xSlice = append(...) } for every other capability interface — there is no equivalent check for ThinkingPrepender. A plugin implementing it is silently never collected.
internal/format/adapter.go:194-196 — separately, CorePluginHooks defines a PrependThinkingToAssistant hook slot, matching the naming/shape of other hooks like MutateCoreRequest.
internal/format/adapter.go:244-246 — WithDefaults() gives it a no-op default, same pattern as every other hook.
Confirmed via full-repo search — hooks.PrependThinkingToAssistant( is never called anywhere in the codebase. Compare to its sibling hook MutateCoreRequest, which is correctly called from four separate protocol adapters at the point outbound requests are built:
internal/protocol/anthropic/adapter.go:262
internal/protocol/chat/adapter.go:68
internal/protocol/google/adapter.go:85
internal/protocol/openai/adapter.go:180
PrependThinkingToAssistant has zero call sites anywhere in the pipeline.
Conclusion
The DeepSeek V4 thinking round-trip logic is fully implemented, tested, and even has a dedicated hook slot reserved for it in the core plugin architecture — but it's disconnected at two separate points:
Registry never collects plugins implementing ThinkingPrepender (no slice, no registration check).
Even if it did, no protocol adapter calls hooks.PrependThinkingToAssistant(...), unlike its sibling MutateCoreRequest, which is wired correctly in 4 places.
Suggested fix
Mirror the existing TransformError/errorTransformers pattern for ThinkingPrepender:
Add thinkingPrependers []ThinkingPrepender to Registry (registry.go ~line 30).
Register it alongside the other capability checks (registry.go ~line 98-107):
go
if v, ok := p.(ThinkingPrepender); ok {
r.thinkingPrependers = append(r.thinkingPrependers, v)
}
Add a dispatch method on Registry, mirroring TransformError's shape, that calls PrependThinkingForAssistant/PrependThinkingForToolUse for each registered plugin enabled for the current model.
Wire hooks.PrependThinkingToAssistant (wherever CorePluginHooks gets constructed per-request/per-model) to call the new Registry dispatch method.
Call a.hooks.PrependThinkingToAssistant(ctx, req) in the same protocol adapters where MutateCoreRequest is already called — at minimum internal/protocol/openai/adapter.go (used by Codex) and internal/protocol/anthropic/adapter.go.
Given the DeepSeek plugin's side of this is already correct and tested, this looks like a contained fix — connecting existing, working pieces rather than writing new logic.