From 168d45d2abb04371d6af2d99fcc303f29902178e Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Sat, 30 May 2026 16:12:42 +1000 Subject: [PATCH] fix(anthropic): legacy budget maps adaptive-only models to adaptive Address codex review (new P1) on #9484: thinking_type() returned Enabled whenever only a legacy ANTHROPIC_THINKING_BUDGET / CLAUDE_THINKING_BUDGET was set, before checking is_adaptive_model. For adaptive-only Opus 4.7/4.8 that produced thinking: {"type":"enabled","budget_tokens":...}, which Anthropic rejects with HTTP 400. Now the legacy-budget path maps adaptive models to the adaptive payload while non-adaptive models keep the enabled/budget behavior. Adds a test covering the legacy-budget-without-effort path. Signed-off-by: Michael Neale --- .../goose/src/providers/formats/anthropic.rs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 8ce6d66ac5..1a9aaadaa5 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -92,7 +92,13 @@ pub fn thinking_type(model_config: &ModelConfig) -> ThinkingType { let effort = model_config.thinking_effort(); if effort.is_none() && legacy_thinking_budget_tokens().is_some() { - return ThinkingType::Enabled; + // Adaptive-only models reject manual budget_tokens, so a legacy budget + // setting must still map to the adaptive payload rather than enabled. + return if is_adaptive_model { + ThinkingType::Adaptive + } else { + ThinkingType::Enabled + }; } match effort.unwrap_or(ThinkingEffort::Off) { @@ -1616,6 +1622,36 @@ mod tests { assert_eq!(thinking_budget_tokens(&config), 8192); } + #[test] + fn test_legacy_budget_maps_adaptive_models_to_adaptive() { + let _guard = env_lock::lock_env([ + ("GOOSE_THINKING_EFFORT", None::<&str>), + // Set to an unrecognized value so env (read first) shadows any + // CLAUDE_THINKING_TYPE in the developer's config file, leaving + // thinking_effort() as None so the legacy-budget branch is exercised. + ("CLAUDE_THINKING_TYPE", Some("unset")), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ("ANTHROPIC_THINKING_BUDGET", Some("8192")), + ("CLAUDE_THINKING_BUDGET", None::<&str>), + ]); + + // Adaptive-only models must use the adaptive payload even when only a + // legacy budget env var is set (no thinking_effort configured). + for model in ["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"] { + assert_eq!( + thinking_type(&cfg(model)), + ThinkingType::Adaptive, + "model {model}" + ); + } + + // Non-adaptive models keep the legacy enabled/budget payload. + assert_eq!( + thinking_type(&cfg("claude-3-7-sonnet-20250219")), + ThinkingType::Enabled + ); + } + #[test] fn test_thinking_type_non_claude_always_disabled() { assert_eq!(