Repro: DeepSeek thinking mode returns 400 — reasoning_content must be passed back
Title (suggestion)
langchain-deepseek drops reasoning_content when building the request → DeepSeek thinking mode returns 400 on multi-turn tool-call histories (still reproduces on v1.1.0)
Environment
- OS: Windows 11
- Python: 3.12.13
langchain: 1.2.12
langchain-core: 1.2.18
langchain-deepseek: 1.1.0 (latest, reproduces) / 1.0.1 (also reproduces)
langchain-openai: 1.1.11
openai: 2.26.0
- Model:
deepseek-v4-flash (thinking mode, enabled by default)
- Base URL:
https://api.deepseek.com
Steps to reproduce
pip install -U langchain-deepseek==1.1.0 langchain-openai
import os
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
load_dotenv() # provides DEEPSEEK_BASE_URL / DEEPSEEK_API_KEY / DEEPSEEK_MODEL
llm = init_chat_model(
base_url=os.getenv("DEEPSEEK_BASE_URL"),
api_key=os.getenv("DEEPSEEK_API_KEY"),
model=os.getenv("DEEPSEEK_MODEL"), # deepseek-v4-flash
)
# Hand-crafted multi-turn history: assistant called a tool, tool replied.
# This is the standard shape an agent loop produces after a tool call.
messages = [
HumanMessage("今天北京天气如何?"),
AIMessage(
content="",
tool_calls=[{
"name": "get_weather",
"args": {"location": "北京"},
"id": "call_00_nUD2NC9QRN5Cg1GaoIkBJQ4s",
}],
),
ToolMessage(
content="今天北京天气晴朗,万里无云~",
tool_call_id="call_00_nUD2NC9QRN5Cg1GaoIkBJQ4s",
),
]
llm.invoke(messages)
Actual result
openai.BadRequestError: Error code: 400 - {'error': {'message': 'The `reasoning_content` in the thinking mode must be passed back to the API.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}}
Expected result
The request should be accepted. The hand-crafted history above is a legitimate tool-call turn; a real agent loop produces exactly this shape and should not hard-fail.
Root cause analysis
langchain_openai._convert_message_to_dict builds the outbound assistant message from content + tool_calls and never merges AIMessage.additional_kwargs (verified against the source of langchain-openai 1.4.2). So even if reasoning_content is present on the message — e.g. captured from a previous DeepSeek response via msg.additional_kwargs["reasoning_content"] — it is dropped before the request is sent.
DeepSeek's official Thinking Mode docs state that for tool-call turns, reasoning_content must participate in context concatenation and must be passed back in all subsequent turns. With thinking mode on, the API rejects the request when it is missing.
Workaround that confirms the diagnosis — disabling thinking mode makes the exact same history pass:
llm = init_chat_model(
base_url=os.getenv("DEEPSEEK_BASE_URL"),
api_key=os.getenv("DEEPSEEK_API_KEY"),
model=os.getenv("DEEPSEEK_MODEL"),
extra_body={"thinking": {"type": "disabled"}},
)
Related, previously-closed PRs
The same fix was attempted multiple times but every PR was closed without merging:
The closing comment claimed the failure "no longer reproduces on langchain-deepseek 1.1.0" and that DeepSeek changed the contract so reasoning_content should be excluded from input. That claim is contradicted by:
- The linked docs page in the closing comment now 404s, and the current Thinking Mode docs still state
reasoning_content must be passed back after tool calls.
- The exact 400 reproduces here on
langchain-deepseek 1.1.0 (the version cited in the closing comment) against the live API on 2026-08-10.
langchain-deepseek 1.1.0 contains no request-side reasoning_content handling — it only reads the field from responses into additional_kwargs; langchain-openai still drops additional_kwargs on outbound conversion.
Suggested fix
In _convert_message_to_dict (or a DeepSeek-specific subclass), forward AIMessage.additional_kwargs["reasoning_content"] into the outbound assistant message dict (empty string is accepted by the API as a placeholder when no reasoning text exists). This is exactly what the closed PRs above implemented.
Repro: DeepSeek thinking mode returns 400 —
reasoning_contentmust be passed backTitle (suggestion)
langchain-deepseekdropsreasoning_contentwhen building the request → DeepSeek thinking mode returns 400 on multi-turn tool-call histories (still reproduces on v1.1.0)Environment
langchain: 1.2.12langchain-core: 1.2.18langchain-deepseek: 1.1.0 (latest, reproduces) / 1.0.1 (also reproduces)langchain-openai: 1.1.11openai: 2.26.0deepseek-v4-flash(thinking mode, enabled by default)https://api.deepseek.comSteps to reproduce
Actual result
Expected result
The request should be accepted. The hand-crafted history above is a legitimate tool-call turn; a real agent loop produces exactly this shape and should not hard-fail.
Root cause analysis
langchain_openai._convert_message_to_dictbuilds the outbound assistant message fromcontent+tool_callsand never mergesAIMessage.additional_kwargs(verified against the source oflangchain-openai1.4.2). So even ifreasoning_contentis present on the message — e.g. captured from a previous DeepSeek response viamsg.additional_kwargs["reasoning_content"]— it is dropped before the request is sent.DeepSeek's official Thinking Mode docs state that for tool-call turns,
reasoning_contentmust participate in context concatenation and must be passed back in all subsequent turns. With thinking mode on, the API rejects the request when it is missing.Workaround that confirms the diagnosis — disabling thinking mode makes the exact same history pass:
Related, previously-closed PRs
The same fix was attempted multiple times but every PR was closed without merging:
langchain_openai: round-trip reasoning_content for OpenAI-compatible providers (DeepSeek fix)— auto-closed (contributor not assigned to issue)fix(deepseek): pass reasoning_content back in multi-turn requests— auto-closed (same reason)feat(deepseek): add reasoning_content support for v3.2 thinking mode tool calls— closed as duplicate of feat(deepseek): support interleaved reasoning in multi-turn tool call loop #34177feat(deepseek): support interleaved reasoning in multi-turn tool call loop— closed 2026-06-03 by maintainerreasoning_contentin multi-turn conversations #34516 / fix(deepseek): preservereasoning_contentforAIMessageswithtool_calls#34438 / fix(deepseek): deepseek-reasoner model cannot returnreasoning_contentwith tools #34092 — same fix, all closed 2026-06-03The closing comment claimed the failure "no longer reproduces on langchain-deepseek 1.1.0" and that DeepSeek changed the contract so
reasoning_contentshould be excluded from input. That claim is contradicted by:reasoning_contentmust be passed back after tool calls.langchain-deepseek 1.1.0(the version cited in the closing comment) against the live API on 2026-08-10.langchain-deepseek1.1.0 contains no request-sidereasoning_contenthandling — it only reads the field from responses intoadditional_kwargs;langchain-openaistill dropsadditional_kwargson outbound conversion.Suggested fix
In
_convert_message_to_dict(or a DeepSeek-specific subclass), forwardAIMessage.additional_kwargs["reasoning_content"]into the outbound assistant message dict (empty string is accepted by the API as a placeholder when no reasoning text exists). This is exactly what the closed PRs above implemented.