diff --git a/internal/agent/compact.go b/internal/agent/compact.go index d215858e7d..ec888b6964 100644 --- a/internal/agent/compact.go +++ b/internal/agent/compact.go @@ -382,6 +382,11 @@ func (a *Agent) summaryRequest(region []provider.Message, instructions string) p Tools: schemas, MaxTokens: a.summaryOutputBudget(), Temperature: provider.OptionalTemperature(a.temperature), + // The compaction prompt explicitly asks for a direct briefing with no + // thinking, and the summary budget must not be consumed by reasoning on + // thinking models. Force reasoning off so compaction works even when the + // user has a reasoning effort configured (#9613). + EffortOverride: "none", } } diff --git a/internal/provider/responses/responses.go b/internal/provider/responses/responses.go index e33846cbe1..abd47e5ef1 100644 --- a/internal/provider/responses/responses.go +++ b/internal/provider/responses/responses.go @@ -268,7 +268,13 @@ func (c *client) buildRequestBody(req provider.Request) (map[string]any, bool, [ messages := provider.SanitizeToolPairing(provider.ModelMessages(req.Messages)) body := map[string]any{"model": c.model, "stream": true} - effort := strings.ToLower(strings.TrimSpace(c.effort)) + // A per-request EffortOverride (e.g. summary/compaction forcing thinking + // off) wins over the client's configured effort; otherwise client effort + // reaches every request it builds (#9613). + effort := strings.ToLower(strings.TrimSpace(req.EffortOverride)) + if effort == "" { + effort = strings.ToLower(strings.TrimSpace(c.effort)) + } if c.vendor == "deepseek" && (strings.EqualFold(strings.TrimSpace(c.model), "deepseek-v4-flash") || strings.EqualFold(strings.TrimSpace(c.model), "deepseek-v4-pro")) { if effort == "medium" || effort == "xhigh" { effort = "high" diff --git a/internal/provider/responses/responses_test.go b/internal/provider/responses/responses_test.go index c77c7ee031..3a610fdec2 100644 --- a/internal/provider/responses/responses_test.go +++ b/internal/provider/responses/responses_test.go @@ -116,6 +116,30 @@ func TestDeepSeekV4ResponsesEffortAliasesNormalizeToHigh(t *testing.T) { } } +// TestEffortOverrideWinsOverClientEffort covers #9613: a per-request +// EffortOverride (used by compaction summary requests to force thinking off) +// must override the client's configured effort in the serialized body, while a +// request without an override still inherits the configured effort. +func TestEffortOverrideWinsOverClientEffort(t *testing.T) { + message := []provider.Message{{Role: provider.RoleUser, Content: "hi"}} + + // No override -> client effort (high) reaches the wire unchanged. + inherited := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-pro", Effort: "high"}).(*client) + body, _, _ := inherited.buildRequestBody(provider.Request{Messages: message}) + if got, _ := body["reasoning"].(map[string]any)["effort"].(string); got != "high" { + t.Fatalf("no override inherited effort = %q, want high", got) + } + + // Override "none" wins even when the client effort is high. + for _, clientEffort := range []string{"high", ""} { + overridden := New(Config{Name: "deepseek", BaseURL: "https://api.deepseek.com", Model: "deepseek-v4-pro", Effort: clientEffort}).(*client) + b, _, _ := overridden.buildRequestBody(provider.Request{Messages: message, EffortOverride: "none"}) + if got, _ := b["reasoning"].(map[string]any)["effort"].(string); got != "none" { + t.Fatalf("override none with client effort %q serialized as %q, want none", clientEffort, got) + } + } +} + func TestRequestSerializesExplicitMaxOutputTokens(t *testing.T) { client := New(Config{Name: "responses", BaseURL: "https://example.com", Model: "model"}).(*client) body, _, _ := client.buildRequestBody(provider.Request{