@@ -58,9 +58,9 @@ func ExtraContentFunc(choice openaisdk.ChatCompletionChoice) []fantasy.Content {
if err != nil {
return content
}
- if reasoningData.ReasoningContent != "" {
+ if rc := reasoningData.GetReasoningContent(); rc != "" {
content = append(content, fantasy.ReasoningContent{
- Text: reasoningData.ReasoningContent,
+ Text: rc,
})
}
return content
@@ -114,11 +114,11 @@ func StreamExtraFunc(chunk openaisdk.ChatCompletionChunk, yield func(fantasy.Str
Delta: reasoningContent,
})
}
- if reasoningData.ReasoningContent != "" {
+ if rc := reasoningData.GetReasoningContent(); rc != "" {
if !reasoningStarted {
ctx[reasoningStartedCtx] = true
}
- return ctx, emitEvent(reasoningData.ReasoningContent)
+ return ctx, emitEvent(rc)
}
if reasoningStarted && (choice.Delta.Content != "" || len(choice.Delta.ToolCalls) > 0) {
ctx[reasoningStartedCtx] = false
@@ -137,6 +137,8 @@ func StreamExtraFunc(chunk openaisdk.ChatCompletionChunk, yield func(fantasy.Str
func ToPromptFunc(prompt fantasy.Prompt, _, _ string) ([]openaisdk.ChatCompletionMessageParamUnion, []fantasy.CallWarning) {
var messages []openaisdk.ChatCompletionMessageParamUnion
var warnings []fantasy.CallWarning
+ hasReasoning := false
+
for _, msg := range prompt {
switch msg.Role {
case fantasy.MessageRoleSystem:
@@ -348,6 +350,7 @@ func ToPromptFunc(prompt fantasy.Prompt, _, _ string) ([]openaisdk.ChatCompletio
continue
}
reasoningText = reasoningPart.Text
+ hasReasoning = true
case fantasy.ContentTypeToolCall:
toolCallPart, ok := fantasy.AsContentType[fantasy.ToolCallPart](c)
if !ok {
@@ -370,8 +373,10 @@ func ToPromptFunc(prompt fantasy.Prompt, _, _ string) ([]openaisdk.ChatCompletio
})
}
}
- // Add reasoning_content field if present
- if reasoningText != "" {
+ // Add reasoning_content field if present, or if thinking is enabled
+ // and the message has tool calls (some providers like Kimi require
+ // reasoning_content on all assistant messages when thinking is enabled).
+ if reasoningText != "" || (hasReasoning && len(assistantMsg.ToolCalls) > 0) {
assistantMsg.SetExtraFields(map[string]any{
"reasoning_content": reasoningText,
})
@@ -347,6 +347,61 @@ func TestToPromptFunc_DropsEmptyMessages(t *testing.T) {
require.Empty(t, warnings)
})
+ t.Run("should add empty reasoning_content to tool call messages when thinking is enabled", func(t *testing.T) {
+ t.Parallel()
+
+ // When thinking is enabled (reasoning parts exist in history),
+ // tool call messages without their own reasoning must still include
+ // reasoning_content. Providers like Kimi require it.
+ prompt := fantasy.Prompt{
+ {
+ Role: fantasy.MessageRoleUser,
+ Content: []fantasy.MessagePart{
+ fantasy.TextPart{Text: "What is 2+2?"},
+ },
+ },
+ {
+ // First turn has reasoning
+ Role: fantasy.MessageRoleAssistant,
+ Content: []fantasy.MessagePart{
+ fantasy.ReasoningPart{Text: "Simple math."},
+ fantasy.TextPart{Text: "Four"},
+ },
+ },
+ {
+ Role: fantasy.MessageRoleUser,
+ Content: []fantasy.MessagePart{
+ fantasy.TextPart{Text: "Now try a tool call"},
+ },
+ },
+ {
+ // Tool call WITHOUT reasoning on this turn
+ Role: fantasy.MessageRoleAssistant,
+ Content: []fantasy.MessagePart{
+ fantasy.ToolCallPart{
+ ToolCallID: "call_1",
+ ToolName: "execute",
+ Input: `{"command":"echo 4"}`,
+ },
+ },
+ },
+ }
+
+ messages, warnings := ToPromptFunc(prompt, "", "")
+
+ require.Empty(t, warnings)
+ require.Len(t, messages, 4)
+
+ // Tool call message must have reasoning_content (empty) since
+ // thinking is enabled in this conversation
+ msg := messages[3].OfAssistant
+ require.NotNil(t, msg)
+ extraFields := msg.ExtraFields()
+ reasoningContent, hasReasoning := extraFields["reasoning_content"]
+ require.True(t, hasReasoning, "reasoning_content must be present on tool call messages when thinking is enabled")
+ require.Equal(t, "", reasoningContent)
+ })
+
t.Run("should drop user messages without visible content", func(t *testing.T) {
t.Parallel()
@@ -31,8 +31,18 @@ type ProviderOptions struct {
}
// ReasoningData represents reasoning data for OpenAI-compatible provider.
+// Some providers use "reasoning_content" (e.g. Avian), others use "reasoning" (e.g. Moonshot AI/Kimi).
type ReasoningData struct {
ReasoningContent string `json:"reasoning_content"`
+ Reasoning string `json:"reasoning"`
+}
+
+// GetReasoningContent returns the reasoning text from whichever field is populated.
+func (r ReasoningData) GetReasoningContent() string {
+ if r.ReasoningContent != "" {
+ return r.ReasoningContent
+ }
+ return r.Reasoning
}
// Options implements the ProviderOptions interface.