-
Notifications
You must be signed in to change notification settings - Fork 2.7k
fix: preserve reasoning_content for thinking models in multi-turn conversations #159
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,7 +127,8 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) { | |
| var apiResponse struct { | ||
| Choices []struct { | ||
| Message struct { | ||
| Content string `json:"content"` | ||
| Content string `json:"content"` | ||
| ReasoningContent string `json:"reasoning_content"` // For thinking models (e.g., GLM-Z1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [H2 — HIGH: No Test Coverage] @siciyuan404 — This is the core parsing logic for the new feature, but there's no unit test that validates The existing func TestParseResponse_ReasoningContent(t *testing.T) {
body := `{"choices":[{"message":{"content":"answer","reasoning_content":"thinking process"},"finish_reason":"stop"}]}`
p := &HTTPProvider{}
resp, err := p.parseResponse([]byte(body))
if err != nil {
t.Fatalf("parseResponse() error: %v", err)
}
if resp.ReasoningContent != "thinking process" {
t.Errorf("ReasoningContent = %q, want %q", resp.ReasoningContent, "thinking process")
}
}Also need a test that verifies Without tests, this fix could regress silently in future refactors (especially with PR #213 restructuring these files). |
||
| ToolCalls []struct { | ||
| ID string `json:"id"` | ||
| Type string `json:"type"` | ||
|
|
@@ -186,10 +187,11 @@ func (p *HTTPProvider) parseResponse(body []byte) (*LLMResponse, error) { | |
| } | ||
|
|
||
| return &LLMResponse{ | ||
| Content: choice.Message.Content, | ||
| ToolCalls: toolCalls, | ||
| FinishReason: choice.FinishReason, | ||
| Usage: apiResponse.Usage, | ||
| Content: choice.Message.Content, | ||
| ReasoningContent: choice.Message.ReasoningContent, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [H3 — HIGH: Claude Provider Gap] @siciyuan404 — You correctly added
While this is a pre-existing issue and Claude uses a different mechanism than case "thinking":
tb := block.AsThinking()
// Store in ReasoningContent for consistency
reasoningContent += tb.ThinkingThis would give PicoClaw consistent thinking-model support across both protocol families (OpenAI-compat and Anthropic). If you'd prefer to keep this PR focused, at minimum please open a follow-up issue tracking this gap. |
||
| ToolCalls: toolCalls, | ||
| FinishReason: choice.FinishReason, | ||
| Usage: apiResponse.Usage, | ||
| }, nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,10 +16,11 @@ type FunctionCall struct { | |
| } | ||
|
|
||
| type LLMResponse struct { | ||
| Content string `json:"content"` | ||
| ToolCalls []ToolCall `json:"tool_calls,omitempty"` | ||
| FinishReason string `json:"finish_reason"` | ||
| Usage *UsageInfo `json:"usage,omitempty"` | ||
| Content string `json:"content"` | ||
| ReasoningContent string `json:"reasoning_content,omitempty"` // For thinking models (e.g., GLM-Z1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [H1 — HIGH: Merge Conflict with PR #213] @siciyuan404 — This file ( If PR #213 merges first: Your changes here will need to be applied to the If this PR merges first: PR #213 will need to propagate Recommendation: Coordinate with @jmahotiedu (PR #213 author) on merge order. Given that this is a small bug fix and #213 is a larger refactor, I'd suggest merging this first and having #213 pick up the new field. |
||
| ToolCalls []ToolCall `json:"tool_calls,omitempty"` | ||
| FinishReason string `json:"finish_reason"` | ||
| Usage *UsageInfo `json:"usage,omitempty"` | ||
| } | ||
|
|
||
| type UsageInfo struct { | ||
|
|
@@ -29,10 +30,11 @@ type UsageInfo struct { | |
| } | ||
|
|
||
| type Message struct { | ||
| Role string `json:"role"` | ||
| Content string `json:"content"` | ||
| ToolCalls []ToolCall `json:"tool_calls,omitempty"` | ||
| ToolCallID string `json:"tool_call_id,omitempty"` | ||
| Role string `json:"role"` | ||
| Content string `json:"content"` | ||
| ReasoningContent string `json:"reasoning_content,omitempty"` // For thinking models (e.g., GLM-Z1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [M1 — MEDIUM: Provider-Specific Field] @siciyuan404 — The
The comment says "e.g., GLM-Z1" which is accurate but incomplete. Consider updating to: ReasoningContent string `json:"reasoning_content,omitempty"` // For thinking models (Zhipu GLM-Z1, Kimi K2.5, DeepSeek R1)Also, if other OpenAI-compatible providers start using different field names for reasoning output, this approach won't scale. Worth adding a note in the code or docs about this limitation. |
||
| ToolCalls []ToolCall `json:"tool_calls,omitempty"` | ||
| ToolCallID string `json:"tool_call_id,omitempty"` | ||
| } | ||
|
|
||
| type LLMProvider interface { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -97,8 +97,9 @@ func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []provider | |
|
|
||
| // 6. Build assistant message with tool calls | ||
| assistantMsg := providers.Message{ | ||
| Role: "assistant", | ||
| Content: response.Content, | ||
| Role: "assistant", | ||
| Content: response.Content, | ||
| ReasoningContent: response.ReasoningContent, // Preserve for thinking models (e.g., GLM-Z1) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Positive] @siciyuan404 — Correctly mirrored the fix from |
||
| } | ||
| for _, tc := range response.ToolCalls { | ||
| argumentsJSON, _ := json.Marshal(tc.Arguments) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Positive] @siciyuan404 — Good catch propagating
ReasoningContentfrom the response into the assistant message. This is the critical path that was causing the API errors. The fact that you also caught the same pattern intoolloop.goshows thoroughness.