Skip to content

Commit 9efdde2

Browse files
winterfxclaude
andcommitted
fix: preserve reasoning_content in multi-turn conversation history
The openaiMessage struct and stripSystemParts() were not carrying over the ReasoningContent field when serializing conversation history for API requests. This caused thinking models (e.g. kimi-k2.5) to receive incomplete assistant messages on subsequent turns, resulting in 400 errors from the Moonshot API. Add the ReasoningContent field to openaiMessage and copy it in stripSystemParts(). Also add a test to verify reasoning_content is preserved when sending conversation history. Fixes #588 Related: #876 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f7136b6 commit 9efdde2

File tree

2 files changed

+60
-8
lines changed

2 files changed

+60
-8
lines changed

pkg/providers/openai_compat/provider.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,11 @@ func parseResponse(body []byte) (*LLMResponse, error) {
289289
// It mirrors protocoltypes.Message but omits SystemParts, which is an
290290
// internal field that would be unknown to third-party endpoints.
291291
type openaiMessage struct {
292-
Role string `json:"role"`
293-
Content string `json:"content"`
294-
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
295-
ToolCallID string `json:"tool_call_id,omitempty"`
292+
Role string `json:"role"`
293+
Content string `json:"content"`
294+
ReasoningContent string `json:"reasoning_content,omitempty"`
295+
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
296+
ToolCallID string `json:"tool_call_id,omitempty"`
296297
}
297298

298299
// stripSystemParts converts []Message to []openaiMessage, dropping the
@@ -302,10 +303,11 @@ func stripSystemParts(messages []Message) []openaiMessage {
302303
out := make([]openaiMessage, len(messages))
303304
for i, m := range messages {
304305
out[i] = openaiMessage{
305-
Role: m.Role,
306-
Content: m.Content,
307-
ToolCalls: m.ToolCalls,
308-
ToolCallID: m.ToolCallID,
306+
Role: m.Role,
307+
Content: m.Content,
308+
ReasoningContent: m.ReasoningContent,
309+
ToolCalls: m.ToolCalls,
310+
ToolCallID: m.ToolCallID,
309311
}
310312
}
311313
return out

pkg/providers/openai_compat/provider_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,56 @@ func TestProviderChat_ParsesReasoningContent(t *testing.T) {
146146
}
147147
}
148148

149+
func TestProviderChat_PreservesReasoningContentInHistory(t *testing.T) {
150+
var requestBody map[string]any
151+
152+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
153+
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
154+
http.Error(w, err.Error(), http.StatusBadRequest)
155+
return
156+
}
157+
resp := map[string]any{
158+
"choices": []map[string]any{
159+
{
160+
"message": map[string]any{"content": "ok"},
161+
"finish_reason": "stop",
162+
},
163+
},
164+
}
165+
w.Header().Set("Content-Type", "application/json")
166+
json.NewEncoder(w).Encode(resp)
167+
}))
168+
defer server.Close()
169+
170+
p := NewProvider("key", server.URL, "")
171+
172+
// Simulate a multi-turn conversation where the assistant's previous
173+
// reply included reasoning_content (e.g. from kimi-k2.5).
174+
messages := []Message{
175+
{Role: "user", Content: "What is 1+1?"},
176+
{Role: "assistant", Content: "2", ReasoningContent: "Let me think... 1+1=2"},
177+
{Role: "user", Content: "What about 2+2?"},
178+
}
179+
180+
_, err := p.Chat(t.Context(), messages, nil, "kimi-k2.5", nil)
181+
if err != nil {
182+
t.Fatalf("Chat() error = %v", err)
183+
}
184+
185+
// Verify reasoning_content is preserved in the serialized request.
186+
reqMessages, ok := requestBody["messages"].([]any)
187+
if !ok {
188+
t.Fatalf("messages is not []any: %T", requestBody["messages"])
189+
}
190+
assistantMsg, ok := reqMessages[1].(map[string]any)
191+
if !ok {
192+
t.Fatalf("assistant message is not map[string]any: %T", reqMessages[1])
193+
}
194+
if assistantMsg["reasoning_content"] != "Let me think... 1+1=2" {
195+
t.Errorf("reasoning_content not preserved in request, got %v", assistantMsg["reasoning_content"])
196+
}
197+
}
198+
149199
func TestProviderChat_HTTPError(t *testing.T) {
150200
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
151201
http.Error(w, "bad request", http.StatusBadRequest)

0 commit comments

Comments
 (0)