Skip to content

llms/openai: reasoning_content lost during tool calling round-trip with deepseek-reasoner #1478

Description

@krus210

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:

  1. Send user message with tools -> API returns assistant message with reasoning_content + tool_calls
  2. 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:

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions