diff --git a/internal/thinking/summary.go b/internal/thinking/summary.go index 430a35d8..5cca0a9b 100644 --- a/internal/thinking/summary.go +++ b/internal/thinking/summary.go @@ -126,11 +126,19 @@ func applySummaryConfigForModel(body []byte, format, model string, modelInfo *re body = applyOpenAIChatSummaryConfig(body, model, enabled) case "claude": // Anthropic documents display as invalid with thinking.type=disabled and - // requires it alongside adaptive or enabled thinking. An enabled source - // summary needs an active target thinking mode. A disabled summary only - // hides an already-active target thinking mode; it must not enable thinking - // merely to hide a summary that would not otherwise exist. Unspecified - // intent returns above and leaves the target's default untouched. + // requires it alongside adaptive or enabled thinking. Model defaults differ: + // Opus 5 and Sonnet 5 default to adaptive thinking; Fable/Mythos 5 are always + // on. Opus 4.8/4.7/4.6, Sonnet 4.6, and the 4.5 models default to thinking + // off. The newest models also default display to omitted. Keeping a missing + // thinking block absent therefore preserves both kinds of model default; + // absence does not mean every Claude model runs without thinking. Only an + // enabled summary may activate a valid target thinking mode so that summarized + // text can be returned. A disabled summary only adds omitted to an + // already-active target mode. + // + // Anthropic docs: + // https://platform.claude.com/docs/en/build-with-claude/thinking + // https://platform.claude.com/docs/en/build-with-claude/thinking-troubleshooting#supported-models if enabled && !gjson.GetBytes(body, "thinking.type").Exists() { body = enableClaudeThinkingForSummary(body, model, modelInfo) } diff --git a/internal/thinking/summary_test.go b/internal/thinking/summary_test.go index 59bd2eb4..1fed4147 100644 --- a/internal/thinking/summary_test.go +++ b/internal/thinking/summary_test.go @@ -176,8 +176,9 @@ func TestApplySummaryConfigForModel_ClaudeEnabledSummaryUsesValidThinkingMode(t } } -// Disabling summaries must not activate Claude thinking. Doing so would add -// reasoning tokens, latency, and cost to a request that asked only to hide output. +// Disabling summaries must not make CPA add a Claude thinking block. Absence +// preserves the per-model default: newer models may still think by default, +// while older models remain off. func TestApplySummaryConfigForModel_ClaudeDisabledSummaryDoesNotEnableThinking(t *testing.T) { for _, model := range []string{"claude-opus-5", "claude-haiku-4-5-20251001"} { body := []byte(`{"model":"` + model + `","max_tokens":32000}`) diff --git a/test/summary_intent_translation_test.go b/test/summary_intent_translation_test.go index 5cb01362..b53c7ca3 100644 --- a/test/summary_intent_translation_test.go +++ b/test/summary_intent_translation_test.go @@ -119,9 +119,10 @@ func TestSummaryIntentFinalPipeline(t *testing.T) { wantExists bool }{ {name: "Responses summary only activates visible Claude thinking", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","reasoning":{"summary":"auto"},"input":"hi"}`, path: "thinking.display", want: "summarized", wantExists: true}, - // Disabling summaries alone must not activate Claude thinking: doing so adds - // reasoning tokens, latency, and cost to a request with no thinking effort. - {name: "Responses null summary alone keeps Claude thinking disabled", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","reasoning":{"summary":null},"input":"hi"}`, path: "thinking"}, + // Summary visibility must not override Claude's per-model thinking default. + // Sonnet 4.6 defaults off; newer default-on models remain default-on without + // CPA injecting an explicit thinking block. + {name: "Responses null summary alone preserves Claude thinking default", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","reasoning":{"summary":null},"input":"hi"}`, path: "thinking"}, {name: "Responses default keeps Claude display default", from: sdktranslator.FormatOpenAIResponse, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","input":"hi"}`, path: "thinking.display"}, {name: "Chat summary alias only activates valid Claude thinking", from: sdktranslator.FormatOpenAI, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","reasoning":{"exclude":false},"messages":[{"role":"user","content":"hi"}]}`, path: "thinking.display", want: "summarized", wantExists: true}, {name: "Interactions summary only activates valid Claude thinking", from: sdktranslator.FormatInteractions, to: sdktranslator.FormatClaude, model: "claude-sonnet-4-6-model", body: `{"model":"claude-sonnet-4-6-model","generation_config":{"thinking_summaries":"auto"},"input":"hi"}`, path: "thinking.display", want: "summarized", wantExists: true},