cbaf41f50c
Introduces an opt-in level-based reasoning toggle (low/medium/high) that each provider translates to its native parameter: - Anthropic: thinking.budget_tokens (1024/8000/24000), with temperature forced to default and MaxTokens auto-grown above the budget. - OpenAI/xAI/Groq via openaicompat: reasoning_effort string, gated by a new Rules.SupportsReasoning predicate so non-reasoning models don't receive the parameter. xAI uses Rules.MapReasoningEffort to remap "medium" to "high" since its API only accepts low|high. - Google: thinking_config.thinking_budget + include_thoughts:true. - DeepSeek: SupportsReasoning=false (reasoner is always-on; the reasoning_content trace was already extracted via openaicompat). Reasoning content is surfaced as Response.Thinking on Complete and as StreamEventThinking deltas during streaming. Provider-side: extracted from Anthropic thinking content blocks, Google's part.Thought=true parts, and the non-standard reasoning_content field that DeepSeek and Groq emit (parsed out of raw JSON since openai-go doesn't type it). Public API: - llm.ReasoningLevel + ReasoningLow/Medium/High constants - llm.WithReasoning(level) request option - Model.WithReasoning(level) for baked-in defaults - provider.Request.Reasoning, provider.Response.Thinking - provider.StreamEventThinking Tests cover Rules-based gating, MapReasoningEffort, reasoning_content extraction (Complete + Stream), Anthropic budget mapping, and temperature suppression when thinking is enabled. Existing behavior is unchanged when Reasoning is the empty string. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
84 lines
2.2 KiB
Go
84 lines
2.2 KiB
Go
package anthropic
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/go-llm/v2/provider"
|
|
|
|
anth "github.com/liushuangls/go-anthropic/v2"
|
|
)
|
|
|
|
func TestBuildRequest_ThinkingByLevel(t *testing.T) {
|
|
p := New("k")
|
|
cases := []struct {
|
|
level string
|
|
wantBudget int
|
|
}{
|
|
{"", 0},
|
|
{"low", thinkingBudgetLow},
|
|
{"medium", thinkingBudgetMedium},
|
|
{"high", thinkingBudgetHigh},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run("level="+tc.level, func(t *testing.T) {
|
|
req := provider.Request{
|
|
Model: "claude-opus-4-7",
|
|
Reasoning: tc.level,
|
|
Messages: []provider.Message{{Role: "user", Content: "hi"}},
|
|
}
|
|
out := p.buildRequest(req)
|
|
if tc.wantBudget == 0 {
|
|
if out.Thinking != nil {
|
|
t.Fatalf("Thinking should be nil for level=%q, got %+v", tc.level, out.Thinking)
|
|
}
|
|
return
|
|
}
|
|
if out.Thinking == nil {
|
|
t.Fatalf("Thinking should be set for level=%q", tc.level)
|
|
}
|
|
if out.Thinking.Type != anth.ThinkingTypeEnabled {
|
|
t.Errorf("Thinking.Type = %q, want enabled", out.Thinking.Type)
|
|
}
|
|
if out.Thinking.BudgetTokens != tc.wantBudget {
|
|
t.Errorf("BudgetTokens = %d, want %d", out.Thinking.BudgetTokens, tc.wantBudget)
|
|
}
|
|
if out.MaxTokens <= tc.wantBudget {
|
|
t.Errorf("MaxTokens (%d) must exceed BudgetTokens (%d)", out.MaxTokens, tc.wantBudget)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildRequest_ThinkingDropsTemperature(t *testing.T) {
|
|
p := New("k")
|
|
temp := 0.7
|
|
req := provider.Request{
|
|
Model: "claude-opus-4-7",
|
|
Reasoning: "high",
|
|
Temperature: &temp,
|
|
Messages: []provider.Message{{Role: "user", Content: "hi"}},
|
|
}
|
|
out := p.buildRequest(req)
|
|
if out.Temperature != nil {
|
|
t.Errorf("Temperature should be dropped when thinking is enabled, got %v", *out.Temperature)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequest_NoThinkingPreservesTemperature(t *testing.T) {
|
|
p := New("k")
|
|
temp := 0.7
|
|
req := provider.Request{
|
|
Model: "claude-opus-4-7",
|
|
Temperature: &temp,
|
|
Messages: []provider.Message{{Role: "user", Content: "hi"}},
|
|
}
|
|
out := p.buildRequest(req)
|
|
if out.Temperature == nil {
|
|
t.Fatal("Temperature should be set when thinking is disabled")
|
|
}
|
|
got := float64(*out.Temperature)
|
|
if got < 0.69 || got > 0.71 {
|
|
t.Errorf("Temperature should be ~0.7 when thinking is disabled, got %v", got)
|
|
}
|
|
}
|