package anthropic import ( "strings" "testing" api "git.flytoex.net/yuanwei/flyto-agent/internal/transport" "git.flytoex.net/yuanwei/flyto-agent/pkg/flyto" ) // TestBuildRequest_TemperatureTopP_Forwarded 锁 buildRequest 把 // Request.Temperature/TopP 直接透传到 api.MessageRequest 字段. // // Locks buildRequest forwarding Request.Temperature/TopP straight into // the api.MessageRequest fields. func TestBuildRequest_TemperatureTopP_Forwarded(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "claude-sonnet-4-6", MaxTokens: 100, Temperature: flyto.Float(0.3), TopP: flyto.Float(0.85), Messages: []flyto.Message{{Role: flyto.RoleUser, Blocks: []flyto.Block{flyto.TextBlock("hi")}}}, } apiReq, err := p.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } if apiReq.Temperature == nil || *apiReq.Temperature != 0.3 { t.Errorf("apiReq.Temperature = %v, want 0.3", apiReq.Temperature) } if apiReq.TopP == nil || *apiReq.TopP != 0.85 { t.Errorf("apiReq.TopP = %v, want 0.85", apiReq.TopP) } } // TestBuildRequest_NilSampling 锁未设时 apiReq 字段为 nil (omitempty 兜底 // 不传 wire 字段, 上游用 provider 默认). // // Locks the nil case: unset sampling knobs leave apiReq fields nil, which // omitempty later strips from the wire so upstream uses its defaults. func TestBuildRequest_NilSampling(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "claude-sonnet-4-6", MaxTokens: 100, Messages: []flyto.Message{{Role: flyto.RoleUser, Blocks: []flyto.Block{flyto.TextBlock("hi")}}}, } apiReq, err := p.buildRequest(req) if err != nil { t.Fatalf("buildRequest: %v", err) } if apiReq.Temperature != nil { t.Errorf("apiReq.Temperature = %v, want nil", *apiReq.Temperature) } if apiReq.TopP != nil { t.Errorf("apiReq.TopP = %v, want nil", *apiReq.TopP) } } // TestApplyThinkingSamplingConstraints_NoThinking_NoOp 锁 thinking 未启用 // 时 helper 不动 apiReq 字段 (绝大多数请求走此分支). // // Locks the no-op path: when thinking is not enabled, the helper leaves // apiReq Temperature/TopP untouched (the common case for most requests). func TestApplyThinkingSamplingConstraints_NoThinking_NoOp(t *testing.T) { apiReq := &api.MessageRequest{ Temperature: flyto.Float(0.3), TopP: flyto.Float(0.5), } applyThinkingSamplingConstraints(apiReq) if *apiReq.Temperature != 0.3 { t.Errorf("Temperature mutated without thinking: got %v", *apiReq.Temperature) } if *apiReq.TopP != 0.5 { t.Errorf("TopP mutated without thinking: got %v", *apiReq.TopP) } } // TestApplyThinkingSamplingConstraints_TemperatureOverride 锁 thinking // 启用时, temperature != 1.0 被覆盖为 1.0 (服务端硬约束). // // Locks the override behavior: when thinking is enabled, a non-1.0 // temperature is rewritten to 1.0 to comply with Anthropic's server-side // constraint. func TestApplyThinkingSamplingConstraints_TemperatureOverride(t *testing.T) { apiReq := &api.MessageRequest{ Thinking: &api.ThinkingConfig{Type: "enabled", BudgetTokens: 8000}, Temperature: flyto.Float(0.3), } applyThinkingSamplingConstraints(apiReq) if apiReq.Temperature == nil || *apiReq.Temperature != 1.0 { t.Errorf("Temperature with thinking + 0.3 = %v, want 1.0", apiReq.Temperature) } } // TestApplyThinkingSamplingConstraints_TopPBelowFloor_Override 锁 thinking // 启用时, top_p < 0.95 被覆盖为 1.0 (Anthropic API 限制 [0.95, 1.0]). // // Locks the top_p override: with thinking on, top_p < 0.95 is rewritten // to 1.0 (Anthropic restricts top_p to [0.95, 1.0] in thinking mode). func TestApplyThinkingSamplingConstraints_TopPBelowFloor_Override(t *testing.T) { apiReq := &api.MessageRequest{ Thinking: &api.ThinkingConfig{Type: "enabled", BudgetTokens: 8000}, TopP: flyto.Float(0.5), } applyThinkingSamplingConstraints(apiReq) if apiReq.TopP == nil || *apiReq.TopP != 1.0 { t.Errorf("TopP with thinking + 0.5 = %v, want 1.0 (override)", apiReq.TopP) } } // TestApplyThinkingSamplingConstraints_TopPInWindow_Untouched 锁 // thinking 启用时, top_p 在 [0.95, 1.0] 窗口内不动 (合法值不覆盖, 否则会丢失 // 调用方意图). // // Locks: with thinking on, a top_p already inside [0.95, 1.0] is not // rewritten -- legal values must survive the override path or caller // intent is lost. func TestApplyThinkingSamplingConstraints_TopPInWindow_Untouched(t *testing.T) { apiReq := &api.MessageRequest{ Thinking: &api.ThinkingConfig{Type: "enabled", BudgetTokens: 8000}, TopP: flyto.Float(0.97), } applyThinkingSamplingConstraints(apiReq) if *apiReq.TopP != 0.97 { t.Errorf("TopP 0.97 (in [0.95, 1.0]) was overridden to %v", *apiReq.TopP) } } // TestApplyThinkingSamplingConstraints_TemperatureOne_Untouched 锁 // thinking 启用时, temperature == 1.0 不动 (合法值原地保留). // // Locks: with thinking on, temperature == 1.0 (the only legal value) is // not rewritten. func TestApplyThinkingSamplingConstraints_TemperatureOne_Untouched(t *testing.T) { apiReq := &api.MessageRequest{ Thinking: &api.ThinkingConfig{Type: "enabled", BudgetTokens: 8000}, Temperature: flyto.Float(1.0), } applyThinkingSamplingConstraints(apiReq) if *apiReq.Temperature != 1.0 { t.Errorf("Temperature 1.0 (legal) was modified to %v", *apiReq.Temperature) } } // TestDetectFeatureWarnings_ThinkingTemperatureOverride 锁 detectFeatureWarnings // 在 thinking + temp != 1.0 时发出 parameter_overridden warning, message // 含被覆盖的具体值, Detail 含 model + parameter 名 (运维 grep 友好). // // Locks the WarningEvent emitted when thinking + non-1.0 temperature // triggers an override: code = parameter_overridden, message includes // the overridden value, Detail includes model + parameter for ops grep. func TestDetectFeatureWarnings_ThinkingTemperatureOverride(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "claude-sonnet-4-6", NeedsThinking: true, Temperature: flyto.Float(0.3), } warnings := p.detectFeatureWarnings(req) var found *flyto.WarningEvent for _, w := range warnings { if w.Code == "parameter_overridden" && strings.Contains(w.Detail, "parameter=temperature") { found = w break } } if found == nil { t.Fatalf("expected parameter_overridden warning for temperature, got %+v", warnings) } if !strings.Contains(found.Message, "0.3") { t.Errorf("warning Message should mention overridden value 0.3, got %q", found.Message) } if !strings.Contains(found.Detail, "claude-sonnet-4-6") { t.Errorf("warning Detail should mention model, got %q", found.Detail) } } // TestDetectFeatureWarnings_ThinkingTopPOverride 锁 thinking + topP < 0.95 // 时发 parameter_overridden warning, parameter=top_p, message 含原值. // // Locks the WarningEvent for the top_p override path: parameter=top_p // in Detail, original value present in Message. func TestDetectFeatureWarnings_ThinkingTopPOverride(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "claude-sonnet-4-6", NeedsThinking: true, TopP: flyto.Float(0.5), } warnings := p.detectFeatureWarnings(req) var found *flyto.WarningEvent for _, w := range warnings { if w.Code == "parameter_overridden" && strings.Contains(w.Detail, "parameter=top_p") { found = w break } } if found == nil { t.Fatalf("expected parameter_overridden warning for top_p, got %+v", warnings) } if !strings.Contains(found.Message, "0.5") { t.Errorf("warning Message should mention overridden value 0.5, got %q", found.Message) } } // TestDetectFeatureWarnings_NoThinking_NoOverrideWarning 锁未启用 thinking // 时绝不发 parameter_overridden warning (绝大多数请求走此分支, 防止 noise). // // Locks: without thinking enabled, no parameter_overridden warning is // emitted -- the common path must stay quiet. func TestDetectFeatureWarnings_NoThinking_NoOverrideWarning(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "claude-sonnet-4-6", // thinking 未启用 Temperature: flyto.Float(0.3), TopP: flyto.Float(0.5), } warnings := p.detectFeatureWarnings(req) for _, w := range warnings { if w.Code == "parameter_overridden" { t.Errorf("unexpected parameter_overridden warning without thinking: %+v", w) } } } // TestDetectFeatureWarnings_ThinkingLegalValues_NoWarning 锁 thinking 启用 // 但 temperature == 1.0 + topP 在 [0.95, 1.0] 时不发 override warning // (合法值不应误报). // // Locks: with thinking on but temperature == 1.0 and top_p in [0.95, 1.0] // (both legal), no override warning is emitted. func TestDetectFeatureWarnings_ThinkingLegalValues_NoWarning(t *testing.T) { p := New(Config{APIKey: "test-key"}) req := &flyto.Request{ Model: "claude-sonnet-4-6", NeedsThinking: true, Temperature: flyto.Float(1.0), TopP: flyto.Float(0.97), } warnings := p.detectFeatureWarnings(req) for _, w := range warnings { if w.Code == "parameter_overridden" { t.Errorf("unexpected override warning for legal thinking values: %+v", w) } } }