mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
fix: preserve reasoning_content for OpenAI-compatible reasoning models
Models like Moonshot kimi-k2.5 and DeepSeek-R1 return a reasoning_content field in assistant messages. When thinking is enabled, the API requires this field to be echoed back in subsequent requests. PicoClaw was silently dropping it, causing 400 errors on tool-call round-trips. - Add ReasoningContent to Message and LLMResponse types - Parse reasoning_content in openai_compat parseResponse() - Carry reasoning_content through assistant tool-call messages - Add unit test for reasoning_content parsing Fixes #588
This commit is contained in:
+3
-2
@@ -622,8 +622,9 @@ func (al *AgentLoop) runLLMIteration(
|
|||||||
|
|
||||||
// Build assistant message with tool calls
|
// Build assistant message with tool calls
|
||||||
assistantMsg := providers.Message{
|
assistantMsg := providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
Content: response.Content,
|
Content: response.Content,
|
||||||
|
ReasoningContent: response.ReasoningContent,
|
||||||
}
|
}
|
||||||
for _, tc := range normalizedToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
argumentsJSON, _ := json.Marshal(tc.Arguments)
|
||||||
|
|||||||
@@ -148,8 +148,9 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
|||||||
var apiResponse struct {
|
var apiResponse struct {
|
||||||
Choices []struct {
|
Choices []struct {
|
||||||
Message struct {
|
Message struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ToolCalls []struct {
|
ReasoningContent string `json:"reasoning_content"`
|
||||||
|
ToolCalls []struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Function *struct {
|
Function *struct {
|
||||||
@@ -221,10 +222,11 @@ func parseResponse(body []byte) (*LLMResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &LLMResponse{
|
return &LLMResponse{
|
||||||
Content: choice.Message.Content,
|
Content: choice.Message.Content,
|
||||||
ToolCalls: toolCalls,
|
ReasoningContent: choice.Message.ReasoningContent,
|
||||||
FinishReason: choice.FinishReason,
|
ToolCalls: toolCalls,
|
||||||
Usage: apiResponse.Usage,
|
FinishReason: choice.FinishReason,
|
||||||
|
Usage: apiResponse.Usage,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,50 @@ func TestProviderChat_ParsesToolCalls(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderChat_ParsesReasoningContent(t *testing.T) {
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
resp := map[string]any{
|
||||||
|
"choices": []map[string]any{
|
||||||
|
{
|
||||||
|
"message": map[string]any{
|
||||||
|
"content": "The answer is 2",
|
||||||
|
"reasoning_content": "Let me think step by step... 1+1=2",
|
||||||
|
"tool_calls": []map[string]any{
|
||||||
|
{
|
||||||
|
"id": "call_1",
|
||||||
|
"type": "function",
|
||||||
|
"function": map[string]any{
|
||||||
|
"name": "calculator",
|
||||||
|
"arguments": "{\"expr\":\"1+1\"}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"finish_reason": "tool_calls",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
p := NewProvider("key", server.URL, "")
|
||||||
|
out, err := p.Chat(t.Context(), []Message{{Role: "user", Content: "1+1=?"}}, nil, "kimi-k2.5", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Chat() error = %v", err)
|
||||||
|
}
|
||||||
|
if out.ReasoningContent != "Let me think step by step... 1+1=2" {
|
||||||
|
t.Fatalf("ReasoningContent = %q, want %q", out.ReasoningContent, "Let me think step by step... 1+1=2")
|
||||||
|
}
|
||||||
|
if out.Content != "The answer is 2" {
|
||||||
|
t.Fatalf("Content = %q, want %q", out.Content, "The answer is 2")
|
||||||
|
}
|
||||||
|
if len(out.ToolCalls) != 1 {
|
||||||
|
t.Fatalf("len(ToolCalls) = %d, want 1", len(out.ToolCalls))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProviderChat_HTTPError(t *testing.T) {
|
func TestProviderChat_HTTPError(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "bad request", http.StatusBadRequest)
|
http.Error(w, "bad request", http.StatusBadRequest)
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ type FunctionCall struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LLMResponse struct {
|
type LLMResponse struct {
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
FinishReason string `json:"finish_reason"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
Usage *UsageInfo `json:"usage,omitempty"`
|
FinishReason string `json:"finish_reason"`
|
||||||
|
Usage *UsageInfo `json:"usage,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UsageInfo struct {
|
type UsageInfo struct {
|
||||||
@@ -38,10 +39,11 @@ type UsageInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
ReasoningContent string `json:"reasoning_content,omitempty"`
|
||||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
|
||||||
|
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolDefinition struct {
|
type ToolDefinition struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user