146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
package backend
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidReasoningEffort(t *testing.T) {
|
|
cases := map[string]bool{
|
|
"": true, // unset means "use default"
|
|
"low": true,
|
|
"medium": true,
|
|
"high": true,
|
|
"LOW": false, // case-sensitive; profiles use lowercase
|
|
"max": false,
|
|
"hihg": false, // typo must be rejected, not silently ignored
|
|
}
|
|
for in, want := range cases {
|
|
if got := ValidReasoningEffort(in); got != want {
|
|
t.Errorf("ValidReasoningEffort(%q) = %v, want %v", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEffortThinkingBudget(t *testing.T) {
|
|
cases := map[string]int{
|
|
EffortLow: 4096,
|
|
EffortMedium: 8192,
|
|
EffortHigh: 16384,
|
|
"": 0,
|
|
"bogus": 0,
|
|
}
|
|
for in, want := range cases {
|
|
if got := EffortThinkingBudget(in); got != want {
|
|
t.Errorf("EffortThinkingBudget(%q) = %d, want %d", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestResolvedThinkingBudget(t *testing.T) {
|
|
// Explicit budget wins over effort.
|
|
p := GenerationParams{ThinkingBudget: 5000, ReasoningEffort: EffortHigh}
|
|
if got := p.ResolvedThinkingBudget(); got != 5000 {
|
|
t.Errorf("explicit budget: got %d, want 5000", got)
|
|
}
|
|
// Derived from effort when budget unset.
|
|
p = GenerationParams{ReasoningEffort: EffortHigh}
|
|
if got := p.ResolvedThinkingBudget(); got != 16384 {
|
|
t.Errorf("derived budget: got %d, want 16384", got)
|
|
}
|
|
// Neither set → 0 (no thinking).
|
|
p = GenerationParams{}
|
|
if got := p.ResolvedThinkingBudget(); got != 0 {
|
|
t.Errorf("unset: got %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
// captureAnthropicRequest drives AnthropicBackend.ChatStream against a test
|
|
// server and returns the decoded request body.
|
|
func captureAnthropicRequest(t *testing.T, params GenerationParams) anthropicRequest {
|
|
t.Helper()
|
|
var raw []byte
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
raw, _ = io.ReadAll(r.Body)
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
// Minimal well-formed stream so the reader terminates cleanly.
|
|
w.Write([]byte("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
b, err := NewAnthropic("key")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
u, _ := url.Parse(srv.URL)
|
|
b.baseURL = u
|
|
|
|
ch, err := b.ChatStream(context.Background(), []Message{{Role: "user", Content: "audit this"}}, nil, params)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for range ch { //nolint:revive
|
|
}
|
|
|
|
var areq anthropicRequest
|
|
if err := json.Unmarshal(raw, &areq); err != nil {
|
|
t.Fatalf("decode request: %v (body=%s)", err, raw)
|
|
}
|
|
return areq
|
|
}
|
|
|
|
func TestAnthropic_EffortEnablesThinking(t *testing.T) {
|
|
temp := 0.3
|
|
areq := captureAnthropicRequest(t, GenerationParams{
|
|
ReasoningEffort: EffortHigh,
|
|
MaxTokens: 8192,
|
|
Temperature: &temp,
|
|
})
|
|
|
|
if areq.Thinking == nil {
|
|
t.Fatal("thinking not enabled for reasoningEffort=high")
|
|
}
|
|
if areq.Thinking.BudgetTokens != 16384 {
|
|
t.Errorf("budget_tokens = %d, want 16384", areq.Thinking.BudgetTokens)
|
|
}
|
|
// max_tokens must be grown above the thinking budget (budget < max_tokens).
|
|
if areq.MaxTokens <= areq.Thinking.BudgetTokens {
|
|
t.Errorf("max_tokens (%d) must exceed thinking budget (%d)", areq.MaxTokens, areq.Thinking.BudgetTokens)
|
|
}
|
|
// Extended thinking requires temperature=1.
|
|
if areq.Temperature == nil || *areq.Temperature != 1.0 {
|
|
t.Errorf("temperature = %v, want 1.0 when thinking enabled", areq.Temperature)
|
|
}
|
|
}
|
|
|
|
func TestAnthropic_NoEffortNoThinking(t *testing.T) {
|
|
temp := 0.3
|
|
areq := captureAnthropicRequest(t, GenerationParams{MaxTokens: 8192, Temperature: &temp})
|
|
if areq.Thinking != nil {
|
|
t.Errorf("thinking should be disabled when no effort/budget set")
|
|
}
|
|
// Temperature passes through untouched when thinking is off.
|
|
if areq.Temperature == nil || *areq.Temperature != 0.3 {
|
|
t.Errorf("temperature = %v, want 0.3", areq.Temperature)
|
|
}
|
|
}
|
|
|
|
func TestAnthropic_ExplicitBudgetOverridesEffort(t *testing.T) {
|
|
areq := captureAnthropicRequest(t, GenerationParams{
|
|
ReasoningEffort: EffortLow,
|
|
ThinkingBudget: 20000,
|
|
MaxTokens: 8192,
|
|
})
|
|
if areq.Thinking == nil || areq.Thinking.BudgetTokens != 20000 {
|
|
t.Fatalf("budget_tokens = %v, want 20000 (explicit overrides effort)", areq.Thinking)
|
|
}
|
|
if areq.MaxTokens <= 20000 {
|
|
t.Errorf("max_tokens (%d) must exceed explicit budget (20000)", areq.MaxTokens)
|
|
}
|
|
}
|