From 7c4ba2219166700becb68d6db35989ebcaa52f69 Mon Sep 17 00:00:00 2001 From: filip <44206832+filipkujawa@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:32:49 +0000 Subject: [PATCH] fix(providers): send explicit thinking disabled to adaptive Claude models (#11177) --- .../src/formats/anthropic.rs | 34 +++++++++++++ .../src/formats/databricks.rs | 7 ++- crates/goose/src/providers/formats/bedrock.rs | 50 +++++++++++-------- 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/crates/goose-provider-types/src/formats/anthropic.rs b/crates/goose-provider-types/src/formats/anthropic.rs index e4b3b5dd6..bd78729a6 100644 --- a/crates/goose-provider-types/src/formats/anthropic.rs +++ b/crates/goose-provider-types/src/formats/anthropic.rs @@ -98,6 +98,12 @@ fn canonical_thinking_mode(provider_name: &str, model_name: &str) -> Option bool { + canonical_thinking_mode(provider_name, model_name) == Some(ThinkingMode::Adaptive) +} + fn canonical_reasoning(provider_name: &str, model_config: &ModelConfig) -> Option { maybe_get_canonical_model(provider_name, &model_config.model_name) .and_then(|model| model.reasoning) @@ -749,6 +755,12 @@ fn apply_thinking_config( thinking.insert("clear_thinking".to_string(), json!(false)); } } + + if !obj.contains_key("thinking") + && requires_explicit_thinking_disable(provider_name, &model_config.model_name) + { + obj.insert("thinking".to_string(), json!({"type": "disabled"})); + } } pub fn create_request( @@ -1623,6 +1635,12 @@ mod tests { assert!(payload.get("thinking").is_none()); assert!(payload.get("output_config").is_none()); + // Adaptive models treat an omitted field as adaptive, so off must be explicit. + let config = cfg_with_effort("claude-opus-5", "off"); + let payload = create_request_with_default_options(&config, "system", &messages, &[])?; + + assert_eq!(payload["thinking"], json!({"type": "disabled"})); + Ok(()) } @@ -1663,6 +1681,22 @@ mod tests { .get("signature") .is_none()); + // Preserved context still wins on models that need an explicit thinking disable. + let mut config = cfg("claude-opus-5"); + config.max_tokens = Some(64000); + let payload = create_request_with_options_provider( + &config, + "system", + &messages, + &[], + AnthropicFormatOptions { + preserve_thinking_context: true, + ..Default::default() + }, + )?; + + assert_eq!(payload["thinking"]["type"], "enabled"); + Ok(()) } diff --git a/crates/goose-provider-types/src/formats/databricks.rs b/crates/goose-provider-types/src/formats/databricks.rs index 579db3a85..35141eb16 100644 --- a/crates/goose-provider-types/src/formats/databricks.rs +++ b/crates/goose-provider-types/src/formats/databricks.rs @@ -1,8 +1,8 @@ use crate::cache_semantics::{apply_chat_payload_breakpoints, CacheSemantics}; use crate::conversation::message::{Message, MessageContentBlock}; use crate::formats::anthropic::{ - adaptive_output_effort, model_supports_temperature, thinking_block_is_stale, - thinking_budget_tokens, thinking_type_for_provider, ThinkingType, + adaptive_output_effort, model_supports_temperature, requires_explicit_thinking_disable, + thinking_block_is_stale, thinking_budget_tokens, thinking_type_for_provider, ThinkingType, }; use crate::model::{is_goose_internal_request_param, ModelConfig}; @@ -292,6 +292,9 @@ fn apply_claude_thinking_config( obj.insert("temperature".to_string(), json!(2)); } ThinkingType::Disabled => { + if requires_explicit_thinking_disable(provider_name, &model_config.model_name) { + obj.insert("thinking".to_string(), json!({ "type": "disabled" })); + } if model_supports_temperature(provider_name, model_config) { if let Some(temp) = model_config.temperature { obj.insert("temperature".to_string(), json!(temp)); diff --git a/crates/goose/src/providers/formats/bedrock.rs b/crates/goose/src/providers/formats/bedrock.rs index c69cce6a5..806783baa 100644 --- a/crates/goose/src/providers/formats/bedrock.rs +++ b/crates/goose/src/providers/formats/bedrock.rs @@ -17,9 +17,9 @@ use crate::conversation::message::{Message, MessageContent}; use crate::providers::bedrock::BEDROCK_PROVIDER_NAME; use crate::providers::canonical::maybe_get_canonical_model; use crate::providers::formats::anthropic::{ - adaptive_output_effort, model_supports_temperature, thinking_block_is_stale, - thinking_budget_tokens, thinking_type_for_provider, ThinkingType, ANTHROPIC_PROVIDER_NAME, - MIN_ANSWER_TOKENS, + adaptive_output_effort, model_supports_temperature, requires_explicit_thinking_disable, + thinking_block_is_stale, thinking_budget_tokens, thinking_type_for_provider, ThinkingType, + ANTHROPIC_PROVIDER_NAME, MIN_ANSWER_TOKENS, }; use crate::utils::sanitize_unicode_tags; use goose_providers::conversation::token_usage::Usage; @@ -30,7 +30,8 @@ use regex::Regex; static BEDROCK_VERSION_SUFFIX_RE: Lazy = Lazy::new(|| Regex::new(r"-v\d+(:\d+)?$").unwrap()); pub fn bedrock_anthropic_thinking_fields(model_config: &ModelConfig) -> Option { - let thinking_type = bedrock_anthropic_thinking_type(model_config); + let anthropic_config = bedrock_anthropic_model_config(model_config)?; + let thinking_type = thinking_type_for_provider(ANTHROPIC_PROVIDER_NAME, &anthropic_config); let thinking = match thinking_type { ThinkingType::Adaptive => Document::Object(HashMap::from([( "type".to_string(), @@ -57,7 +58,18 @@ pub fn bedrock_anthropic_thinking_fields(model_config: &ModelConfig) -> Option return None, + ThinkingType::Disabled => { + if !requires_explicit_thinking_disable( + ANTHROPIC_PROVIDER_NAME, + &anthropic_config.model_name, + ) { + return None; + } + Document::Object(HashMap::from([( + "type".to_string(), + Document::String("disabled".to_string()), + )])) + } }; let mut fields = HashMap::from([("thinking".to_string(), thinking)]); @@ -75,17 +87,13 @@ pub fn bedrock_anthropic_thinking_fields(model_config: &ModelConfig) -> Option ThinkingType { - let Some((_, anthropic_model)) = model_config.model_name.rsplit_once("anthropic.") else { - return ThinkingType::Disabled; - }; +fn bedrock_anthropic_model_config(model_config: &ModelConfig) -> Option { + let (_, anthropic_model) = model_config.model_name.rsplit_once("anthropic.")?; - let anthropic_config = ModelConfig { + Some(ModelConfig { model_name: strip_bedrock_version_suffix(anthropic_model), ..model_config.clone() - }; - - thinking_type_for_provider(ANTHROPIC_PROVIDER_NAME, &anthropic_config) + }) } /// Bedrock model ids carry a `-v1:0` style suffix (e.g. @@ -133,16 +141,11 @@ pub fn bedrock_inference_config(model_config: &ModelConfig) -> bedrock::Inferenc } /// Whether `temperature` may be sent for this Bedrock model. For `anthropic.*` -/// ids we resolve against the Anthropic canonical registry (mapping the model -/// name the same way [`bedrock_anthropic_thinking_type`] does); for other known +/// ids we resolve against the Anthropic canonical registry; for other known /// Bedrock ids we consult the Bedrock canonical registry and otherwise keep the /// permissive fallback used by [`model_supports_temperature`]. fn bedrock_model_supports_temperature(model_config: &ModelConfig) -> bool { - if let Some((_, anthropic_model)) = model_config.model_name.rsplit_once("anthropic.") { - let anthropic_config = ModelConfig { - model_name: strip_bedrock_version_suffix(anthropic_model), - ..model_config.clone() - }; + if let Some(anthropic_config) = bedrock_anthropic_model_config(model_config) { model_supports_temperature(ANTHROPIC_PROVIDER_NAME, &anthropic_config) } else { maybe_get_canonical_model(BEDROCK_PROVIDER_NAME, &model_config.model_name) @@ -702,6 +705,13 @@ mod tests { )])); assert!(bedrock_anthropic_thinking_fields(&config).is_none()); + + config.model_name = "us.anthropic.claude-opus-4-7-20251101-v1:0".to_string(); + let fields = bedrock_anthropic_thinking_fields(&config).expect("thinking fields"); + assert_eq!( + from_bedrock_json(&fields).unwrap(), + json!({ "thinking": {"type": "disabled"} }) + ); } #[test]