reasoning_content lost during round-trip in tool calling with deepseek-reasoner
Problem
When using deepseek-reasoner model with tool calling, the reasoning_content field is lost during the conversation round-trip, causing the DeepSeek API to return HTTP 400:
Missing `reasoning_content` field in the assistant message
Background
The DeepSeek API requires that every assistant message in the conversation history includes the reasoning_content field when using deepseek-reasoner. This is documented at: https://api-docs.deepseek.com/guides/thinking_with_tools
The typical tool calling flow is:
- Send user message with tools -> API returns assistant message with
reasoning_content + tool_calls
- Send back the full history (user msg + assistant msg + tool result) -> API expects
reasoning_content in the assistant message
Root Cause
The reasoning_content field is correctly parsed from the API response into ContentChoice.ReasoningContent (added in PR #1121), but it cannot be sent back in the next request because:
-
MessageContent struct lacks ReasoningContent field (llms/generatecontent.go) -- the struct only has Role and Parts, so there is no way to attach reasoning_content when constructing assistant messages for conversation history.
-
Conversion MessageContent -> ChatMessage doesn't transfer ReasoningContent (llms/openai/openaillm.go) -- even if the field existed, the conversion code for ChatMessageTypeAI only sets msg.Role = RoleAssistant without copying reasoning content.
Note: The internal ChatMessage struct already has the ReasoningContent field and its MarshalJSON correctly serializes it. The gap is only in the public-facing MessageContent struct and the conversion layer.
Reproduction
llm, _ := openai.New(
openai.WithModel("deepseek-reasoner"),
openai.WithToken(apiKey),
openai.WithBaseURL("https://api.deepseek.com/v1"),
)
tools := []llms.Tool{{
Type: "function",
Function: &llms.FunctionDefinition{
Name: "calculator",
Description: "Add two numbers together",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"a": map[string]any{"type": "number"},
"b": map[string]any{"type": "number"},
},
"required": []string{"a", "b"},
},
},
}}
// Step 1: works fine - API returns reasoning_content + tool_call
resp, _ := llm.GenerateContent(ctx, []llms.MessageContent{
{Role: llms.ChatMessageTypeHuman, Parts: []llms.ContentPart{
llms.TextContent{Text: "What is 15 + 28? Use the calculator tool."},
}},
}, llms.WithTools(tools))
choice := resp.Choices[0]
// choice.ReasoningContent is populated correctly
// choice.ToolCalls has the tool call
// Step 2: fails with HTTP 400 because reasoning_content cannot be passed back
assistantMsg := llms.MessageContent{
Role: llms.ChatMessageTypeAI,
Parts: []llms.ContentPart{choice.ToolCalls[0]},
// ReasoningContent: choice.ReasoningContent <-- this field doesn't exist on MessageContent!
}
Expected Behavior
MessageContent should support a ReasoningContent field so that reasoning content from API responses can be preserved in conversation history for subsequent requests.
Environment
reasoning_contentlost during round-trip in tool calling with deepseek-reasonerProblem
When using
deepseek-reasonermodel with tool calling, thereasoning_contentfield is lost during the conversation round-trip, causing the DeepSeek API to return HTTP 400:Background
The DeepSeek API requires that every assistant message in the conversation history includes the
reasoning_contentfield when usingdeepseek-reasoner. This is documented at: https://api-docs.deepseek.com/guides/thinking_with_toolsThe typical tool calling flow is:
reasoning_content+tool_callsreasoning_contentin the assistant messageRoot Cause
The
reasoning_contentfield is correctly parsed from the API response intoContentChoice.ReasoningContent(added in PR #1121), but it cannot be sent back in the next request because:MessageContentstruct lacksReasoningContentfield (llms/generatecontent.go) -- the struct only hasRoleandParts, so there is no way to attachreasoning_contentwhen constructing assistant messages for conversation history.Conversion
MessageContent->ChatMessagedoesn't transferReasoningContent(llms/openai/openaillm.go) -- even if the field existed, the conversion code forChatMessageTypeAIonly setsmsg.Role = RoleAssistantwithout copying reasoning content.Note: The internal
ChatMessagestruct already has theReasoningContentfield and itsMarshalJSONcorrectly serializes it. The gap is only in the public-facingMessageContentstruct and the conversion layer.Reproduction
Expected Behavior
MessageContentshould support aReasoningContentfield so that reasoning content from API responses can be preserved in conversation history for subsequent requests.Environment
reasoning_contentin request payload when using deepseek-reasoner with tool calling langchain-ai/langchain#34166