From bd7f4924bb2065e7ac471ce5b8c1ce47aad1e169 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Thu, 18 Jun 2026 06:44:01 +1000 Subject: [PATCH] fix: pass thinking config to Bedrock Anthropic models (#9794) Signed-off-by: Michael Neale Signed-off-by: Douwe M Osinga Co-authored-by: Douwe M Osinga --- crates/goose/src/providers/bedrock.rs | 16 +- crates/goose/src/providers/formats/bedrock.rs | 166 ++++++++++++++++++ 2 files changed, 179 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index 664b1aa96..44752a144 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -23,12 +23,12 @@ use serde_json::Value; use smithy_transport_reqwest::ReqwestHttpClient; use super::formats::bedrock::{ - from_bedrock_message, from_bedrock_usage, to_bedrock_message_with_caching, - to_bedrock_tool_config, + bedrock_anthropic_thinking_fields, from_bedrock_message, from_bedrock_usage, + to_bedrock_message_with_caching, to_bedrock_tool_config, }; use crate::session_context::SESSION_ID_HEADER; -const BEDROCK_PROVIDER_NAME: &str = "aws_bedrock"; +pub(crate) const BEDROCK_PROVIDER_NAME: &str = "aws_bedrock"; pub const BEDROCK_DOC_LINK: &str = "https://docs.aws.amazon.com/bedrock/latest/userguide/models-supported.html"; @@ -62,6 +62,7 @@ struct ConverseRequestParts { system_blocks: Vec, messages: Vec, tool_config: Option, + thinking_fields: Option, } impl BedrockProvider { @@ -263,6 +264,7 @@ impl BedrockProvider { system_blocks, messages: bedrock_messages, tool_config, + thinking_fields: bedrock_anthropic_thinking_fields(&self.model), }) } @@ -284,6 +286,10 @@ impl BedrockProvider { .model_id(model_name.to_string()) .set_messages(Some(parts.messages)); + if let Some(fields) = parts.thinking_fields { + request = request.additional_model_request_fields(fields); + } + if let Some(tool_config) = parts.tool_config { request = request.tool_config(tool_config); } @@ -376,6 +382,10 @@ impl BedrockProvider { .model_id(model_name.to_string()) .set_messages(Some(parts.messages)); + if let Some(fields) = parts.thinking_fields { + request = request.additional_model_request_fields(fields); + } + if let Some(tool_config) = parts.tool_config { request = request.tool_config(tool_config); } diff --git a/crates/goose/src/providers/formats/bedrock.rs b/crates/goose/src/providers/formats/bedrock.rs index 7c531761d..e65f3bc5f 100644 --- a/crates/goose/src/providers/formats/bedrock.rs +++ b/crates/goose/src/providers/formats/bedrock.rs @@ -15,7 +15,71 @@ use rmcp::model::{ use serde_json::Value; use crate::conversation::message::{Message, MessageContent}; +use crate::model::ModelConfig; +use crate::providers::formats::anthropic::{ + adaptive_output_effort, thinking_budget_tokens, thinking_type_for_provider, ThinkingType, + ANTHROPIC_PROVIDER_NAME, +}; use goose_providers::conversation::token_usage::Usage; +use once_cell::sync::Lazy; +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 thinking = match thinking_type { + ThinkingType::Adaptive => Document::Object(HashMap::from([( + "type".to_string(), + Document::String("adaptive".to_string()), + )])), + ThinkingType::Enabled => Document::Object(HashMap::from([ + ("type".to_string(), Document::String("enabled".to_string())), + ( + "budget_tokens".to_string(), + Document::Number(Number::PosInt(thinking_budget_tokens(model_config) as u64)), + ), + ])), + ThinkingType::Disabled => return None, + }; + + let mut fields = HashMap::from([("thinking".to_string(), thinking)]); + + if thinking_type == ThinkingType::Adaptive { + fields.insert( + "output_config".to_string(), + Document::Object(HashMap::from([( + "effort".to_string(), + Document::String(adaptive_output_effort(model_config).to_string()), + )])), + ); + } + + Some(Document::Object(fields)) +} + +fn bedrock_anthropic_thinking_type(model_config: &ModelConfig) -> ThinkingType { + let Some((_, anthropic_model)) = model_config.model_name.rsplit_once("anthropic.") else { + return ThinkingType::Disabled; + }; + + let anthropic_config = 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. +/// `claude-opus-4-1-20250805-v1:0`) that the canonical Anthropic registry does +/// not recognise. Dropping it lets the date stamp become the terminal segment +/// the registry already knows how to normalise. +fn strip_bedrock_version_suffix(model_name: &str) -> String { + BEDROCK_VERSION_SUFFIX_RE + .replace(model_name, "") + .into_owned() +} pub fn to_bedrock_message_with_caching( message: &Message, @@ -463,6 +527,108 @@ mod tests { use anyhow::Result; use goose_test_support::TEST_IMAGE_B64; use rmcp::model::{AnnotateAble, RawImageContent}; + use serde_json::json; + + #[test] + fn test_bedrock_anthropic_thinking_fields_enabled() { + let mut params = HashMap::new(); + params.insert("thinking_effort".to_string(), json!("low")); + let mut config = ModelConfig::new_or_fail("us.anthropic.claude-3-7-sonnet-20250219-v1:0"); + config.request_params = Some(params); + config.reasoning = Some(true); + + let fields = bedrock_anthropic_thinking_fields(&config).expect("thinking fields"); + assert_eq!( + from_bedrock_json(&fields).unwrap(), + json!({ + "thinking": { + "type": "enabled", + "budget_tokens": 4000 + } + }) + ); + } + + #[test] + fn test_bedrock_anthropic_thinking_fields_disabled() { + let mut config = ModelConfig::new_or_fail("us.anthropic.claude-3-7-sonnet-20250219-v1:0"); + config.reasoning = Some(true); + config.request_params = Some(HashMap::from([( + "thinking_effort".to_string(), + json!("off"), + )])); + + assert!(bedrock_anthropic_thinking_fields(&config).is_none()); + } + + #[test] + fn test_bedrock_anthropic_thinking_fields_always_on_adaptive() { + let mut config = ModelConfig::new_or_fail("global.anthropic.claude-fable-5"); + config.reasoning = Some(true); + config.request_params = Some(HashMap::from([( + "thinking_effort".to_string(), + json!("off"), + )])); + + let fields = bedrock_anthropic_thinking_fields(&config).expect("thinking fields"); + assert_eq!( + from_bedrock_json(&fields).unwrap(), + json!({ + "thinking": {"type": "adaptive"}, + "output_config": {"effort": "high"} + }) + ); + } + + #[test] + fn test_bedrock_anthropic_thinking_fields_adaptive_with_effort() { + let mut config = ModelConfig::new_or_fail("us.anthropic.claude-opus-4.7"); + config.reasoning = Some(true); + config.request_params = Some(HashMap::from([( + "thinking_effort".to_string(), + json!("low"), + )])); + + let fields = bedrock_anthropic_thinking_fields(&config).expect("thinking fields"); + assert_eq!( + from_bedrock_json(&fields).unwrap(), + json!({ + "thinking": {"type": "adaptive"}, + "output_config": {"effort": "low"} + }) + ); + } + + #[test] + fn test_bedrock_anthropic_thinking_fields_adaptive_with_version_suffix() { + let mut config = ModelConfig::new_or_fail("us.anthropic.claude-opus-4-7-20251101-v1:0"); + config.reasoning = Some(true); + config.request_params = Some(HashMap::from([( + "thinking_effort".to_string(), + json!("low"), + )])); + + let fields = bedrock_anthropic_thinking_fields(&config).expect("thinking fields"); + assert_eq!( + from_bedrock_json(&fields).unwrap(), + json!({ + "thinking": {"type": "adaptive"}, + "output_config": {"effort": "low"} + }) + ); + } + + #[test] + fn test_bedrock_thinking_fields_skipped_for_non_anthropic() { + let mut config = ModelConfig::new_or_fail("us.deepseek.r1-v1:0"); + config.reasoning = Some(true); + config.request_params = Some(HashMap::from([( + "thinking_effort".to_string(), + json!("low"), + )])); + + assert!(bedrock_anthropic_thinking_fields(&config).is_none()); + } #[test] fn test_to_bedrock_image_supported_formats() -> Result<()> {