bedrock: Add Claude Opus 5.5 BYOK support (#64685)

## Summary

Add Claude Opus 5.5 to the Amazon Bedrock BYOK provider. Upstream #64627
added Opus 5.5 to the Anthropic provider; this brings the same model to
Bedrock.

The model mirrors the Opus 5 definition: 1M context window, 128K max
output, tool use, image input, prompt caching, and adaptive thinking
(including xhigh effort). Cross-region inference profiles cover global,
us, ca, eu, au, and jp. Opus 5.5 publishes a jp geo profile that Opus 5
does not.

## Model-level behavior

Opus 5.5 carries model-level constraints, matching the Anthropic
provider:

- Opus 5.5 always runs adaptive thinking and rejects an explicit
thinking.type.disabled opt-out, so the provider never sends one. This
also fixes a substring bug where claude-opus-5-5 matched the Opus 5
request id and incorrectly received the opt-out.
- Adaptive thinking does not accept sampling controls, so temperature is
omitted (top_k and top_p were already unset), as the Anthropic provider
does.
- Forced tool use (tool_choice any) is reported as unsupported by
reusing the shared anthropic::supports_forced_tool_use list, since
Bedrock friendly ids match Anthropic model ids. Automatic tool use is
unaffected, and no other Bedrock model changes behavior.

## Tests

- bedrock: model id, request id, display name, token limits, thinking
modes, and cross-region inference (including jp) for Opus 5.5.
- language_models: Opus 5.5 omits the thinking opt-out and temperature,
and rejects only forced tool choice.

Release Notes:

- Added Claude Opus 5.5 support to the Amazon Bedrock provider

---------

Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
Alex Gunnarson 2026-10-09 14:57:03 +00:00 • committed by GitHub
parent efed1f245a
commit df48a139ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 298 additions and 24 deletions

View file

@ -238,7 +238,7 @@ fn thinking_request_fields(thinking: &Thinking) -> HashMap<String, Document> {
),
]);
if let Some(beta_header) = binding_controls_beta {
// Fable 5.1 binds replayed thinking to the prefix, which Zed can change.
// Opus 5.5 and Fable 5.1 bind replayed thinking to the prefix, which Zed can change.
thinking_fields.insert(
"block_binding".to_string(),
value_to_aws_document(&serde_json::json!({

View file

@ -76,6 +76,13 @@ pub enum ConverseModel {
alias = "claude-fable-5-thinking-latest"
)]
ClaudeFable5,
#[serde(
rename = "claude-opus-5-5",
alias = "claude-opus-5-5-latest",
alias = "claude-opus-5-5-thinking",
alias = "claude-opus-5-5-thinking-latest"
)]
ClaudeOpus5_5,
#[serde(
rename = "claude-opus-5",
alias = "claude-opus-5-latest",
@ -275,6 +282,8 @@ impl ConverseModel {
Ok(Self::ClaudeFable5_1)
} else if id.starts_with("claude-fable-5") {
Ok(Self::ClaudeFable5)
} else if id.starts_with("claude-opus-5-5") {
Ok(Self::ClaudeOpus5_5)
} else if id.starts_with("claude-opus-5") {
Ok(Self::ClaudeOpus5)
} else if id.starts_with("claude-opus-4-8") {
@ -306,6 +315,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1 => "claude-fable-5-1",
Self::ClaudeFable5 => "claude-fable-5",
Self::ClaudeOpus5_5 => "claude-opus-5-5",
Self::ClaudeOpus5 => "claude-opus-5",
Self::ClaudeOpus4_8 => "claude-opus-4-8",
Self::ClaudeOpus4_7 => "claude-opus-4-7",
@ -362,6 +372,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1 => "anthropic.claude-fable-5-1",
Self::ClaudeFable5 => "anthropic.claude-fable-5",
Self::ClaudeOpus5_5 => "anthropic.claude-opus-5-5",
Self::ClaudeOpus5 => "anthropic.claude-opus-5",
Self::ClaudeOpus4_8 => "anthropic.claude-opus-4-8",
Self::ClaudeOpus4_7 => "anthropic.claude-opus-4-7",
@ -418,6 +429,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1 => "Claude Fable 5.1",
Self::ClaudeFable5 => "Claude Fable 5",
Self::ClaudeOpus5_5 => "Claude Opus 5.5",
Self::ClaudeOpus5 => "Claude Opus 5",
Self::ClaudeOpus4_8 => "Claude Opus 4.8",
Self::ClaudeOpus4_7 => "Claude Opus 4.7",
@ -476,6 +488,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -516,6 +529,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -571,6 +585,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -611,6 +626,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -638,6 +654,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -662,6 +679,7 @@ impl ConverseModel {
Self::Gpt6Astra
| Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -682,6 +700,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -699,6 +718,7 @@ impl ConverseModel {
match self {
Self::ClaudeFable5_1
| Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeSonnet5 => true,
@ -819,6 +839,7 @@ impl ConverseModel {
let supports_global = matches!(
self,
Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -881,6 +902,7 @@ impl ConverseModel {
// Global inference profiles
(
Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -896,11 +918,14 @@ impl ConverseModel {
) => Ok(format!("{}.{}", region_group, model_id)),
// US Government region inference profiles
(Self::ClaudeSonnet4_5, "us-gov") => Ok(format!("{}.{}", region_group, model_id)),
(Self::ClaudeOpus5_5 | Self::ClaudeSonnet4_5, "us-gov") => {
Ok(format!("{}.{}", region_group, model_id))
}
// US region inference profiles
(
Self::ClaudeFable5
| Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
@ -933,13 +958,15 @@ impl ConverseModel {
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-4-8.html>
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-4-7.html>
(Self::ClaudeOpus5 | Self::ClaudeOpus4_8 | Self::ClaudeOpus4_7, "ca") => {
Ok(format!("us.{}", model_id))
}
(
Self::ClaudeOpus5_5 | Self::ClaudeOpus5 | Self::ClaudeOpus4_8 | Self::ClaudeOpus4_7,
"ca",
) => Ok(format!("us.{}", model_id)),
// EU region inference profiles
(
Self::ClaudeOpus5
Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
| Self::ClaudeOpus4_6
@ -955,7 +982,8 @@ impl ConverseModel {
// Australia region inference profiles
(
Self::ClaudeOpus5
Self::ClaudeOpus5_5
| Self::ClaudeOpus5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
| Self::ClaudeOpus4_6
@ -967,10 +995,13 @@ impl ConverseModel {
// Japan region inference profiles. Claude Opus 5 is deliberately
// absent: its model card lists only `us.`/`eu.`/`au.` geo profiles
// (plus `global.`):
// (plus `global.`). Claude Opus 5.5, unlike Opus 5, does publish a
// `jp.` geo profile:
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5-5.html>
(
Self::ClaudeOpus4_8
Self::ClaudeOpus5_5
| Self::ClaudeOpus4_8
| Self::ClaudeOpus4_7
| Self::ClaudeSonnet4_6
| Self::ClaudeSonnet4_5
@ -990,7 +1021,9 @@ impl ConverseModel {
"apac",
) => Ok(format!("{}.{}", region_group, model_id)),
(Self::ClaudeFable5 | Self::ClaudeSonnet5, _) => Ok(format!("global.{}", model_id)),
(Self::ClaudeFable5 | Self::ClaudeOpus5_5 | Self::ClaudeSonnet5, _) => {
Ok(format!("global.{}", model_id))
}
// Default: use model ID directly
_ => Ok(model_id.into()),
@ -1365,6 +1398,10 @@ mod tests {
ConverseModel::ClaudeOpus5.cross_region_inference_id("us-east-1", false)?,
"us.anthropic.claude-opus-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("us-east-1", false)?,
"us.anthropic.claude-opus-5-5"
);
assert_eq!(
ConverseModel::NovaPro.cross_region_inference_id("us-east-2", false)?,
"us.amazon.nova-pro-v1:0"
@ -1406,6 +1443,10 @@ mod tests {
ConverseModel::ClaudeOpus5.cross_region_inference_id("eu-west-1", false)?,
"eu.anthropic.claude-opus-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("eu-west-1", false)?,
"eu.anthropic.claude-opus-5-5"
);
Ok(())
}
@ -1427,6 +1468,15 @@ mod tests {
ConverseModel::ClaudeSonnet5.cross_region_inference_id("ap-northeast-1", false)?,
"global.anthropic.claude-sonnet-5"
);
// Opus 5.5 has no APAC geo profile outside AU and JP.
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("ap-south-1", false)?,
"global.anthropic.claude-opus-5-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("ap-northeast-2", false)?,
"global.anthropic.claude-opus-5-5"
);
Ok(())
}
@ -1469,6 +1519,10 @@ mod tests {
ConverseModel::ClaudeOpus5.cross_region_inference_id("ap-southeast-2", false)?,
"au.anthropic.claude-opus-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("ap-southeast-2", false)?,
"au.anthropic.claude-opus-5-5"
);
Ok(())
}
@ -1485,6 +1539,11 @@ mod tests {
ConverseModel::ClaudeOpus5.cross_region_inference_id("ap-northeast-1", false)?,
"anthropic.claude-opus-5"
);
// Claude Opus 5.5, unlike Opus 5, publishes a `jp.` geo profile.
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("ap-northeast-1", false)?,
"jp.anthropic.claude-opus-5-5"
);
assert_eq!(
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("ap-northeast-1", false)?,
"jp.anthropic.claude-opus-4-8"
@ -1516,6 +1575,10 @@ mod tests {
ConverseModel::ClaudeOpus5.cross_region_inference_id("ca-central-1", false)?,
"us.anthropic.claude-opus-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("ca-central-1", false)?,
"us.anthropic.claude-opus-5-5"
);
assert_eq!(
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("ca-west-1", false)?,
"us.anthropic.claude-opus-4-8"
@ -1537,6 +1600,10 @@ mod tests {
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("us-gov-west-1", false)?,
"us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("us-gov-west-1", true)?,
"us-gov.anthropic.claude-opus-5-5"
);
Ok(())
}
@ -1570,6 +1637,10 @@ mod tests {
ConverseModel::ClaudeOpus5.cross_region_inference_id("us-east-1", true)?,
"global.anthropic.claude-opus-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.cross_region_inference_id("us-east-1", true)?,
"global.anthropic.claude-opus-5-5"
);
assert_eq!(
ConverseModel::ClaudeFable5.cross_region_inference_id("us-east-1", true)?,
"global.anthropic.claude-fable-5"
@ -1723,6 +1794,7 @@ mod tests {
assert_eq!(ConverseModel::Llama4Scout17B.id(), "llama-4-scout-17b");
assert_eq!(ConverseModel::ClaudeFable5.id(), "claude-fable-5");
assert_eq!(ConverseModel::ClaudeOpus5.id(), "claude-opus-5");
assert_eq!(ConverseModel::ClaudeOpus5_5.id(), "claude-opus-5-5");
assert_eq!(ConverseModel::ClaudeSonnet5.id(), "claude-sonnet-5");
assert_eq!(
@ -1746,6 +1818,10 @@ mod tests {
ConverseModel::ClaudeOpus5.request_id(),
"anthropic.claude-opus-5"
);
assert_eq!(
ConverseModel::ClaudeOpus5_5.request_id(),
"anthropic.claude-opus-5-5"
);
assert_eq!(
ConverseModel::ClaudeSonnet5.request_id(),
"anthropic.claude-sonnet-5"
@ -1771,6 +1847,12 @@ mod tests {
.id(),
"claude-opus-5"
);
assert_eq!(
ConverseModel::from_id("claude-opus-5-5-thinking")
.unwrap()
.id(),
"claude-opus-5-5"
);
assert_eq!(
ConverseModel::from_id("claude-sonnet-5-thinking")
.unwrap()
@ -1797,6 +1879,8 @@ mod tests {
assert!(!ConverseModel::ClaudeOpus4_7.supports_xhigh_adaptive_thinking());
assert!(ConverseModel::ClaudeFable5.supports_xhigh_adaptive_thinking());
assert!(ConverseModel::ClaudeOpus5.supports_xhigh_adaptive_thinking());
assert!(ConverseModel::ClaudeOpus5_5.supports_xhigh_adaptive_thinking());
assert!(ConverseModel::ClaudeOpus5_5.supports_adaptive_thinking());
assert!(ConverseModel::ClaudeSonnet5.supports_xhigh_adaptive_thinking());
assert!(ConverseModel::ClaudeOpus4_8.supports_xhigh_adaptive_thinking());
assert_eq!(BedrockAdaptiveThinkingEffort::XHigh.as_str(), "xhigh");
@ -1827,6 +1911,7 @@ mod tests {
assert_eq!(ConverseModel::ClaudeOpus4_6.max_token_count(), 1_000_000);
assert_eq!(ConverseModel::ClaudeFable5.max_token_count(), 1_000_000);
assert_eq!(ConverseModel::ClaudeOpus5.max_token_count(), 1_000_000);
assert_eq!(ConverseModel::ClaudeOpus5_5.max_token_count(), 1_000_000);
assert_eq!(ConverseModel::ClaudeSonnet5.max_token_count(), 1_000_000);
assert_eq!(ConverseModel::Llama4Scout17B.max_token_count(), 128_000);
assert_eq!(ConverseModel::NovaPremier.max_token_count(), 1_000_000);
@ -1838,6 +1923,7 @@ mod tests {
assert_eq!(ConverseModel::ClaudeOpus4_6.max_output_tokens(), 128_000);
assert_eq!(ConverseModel::ClaudeFable5.max_output_tokens(), 128_000);
assert_eq!(ConverseModel::ClaudeOpus5.max_output_tokens(), 128_000);
assert_eq!(ConverseModel::ClaudeOpus5_5.max_output_tokens(), 128_000);
assert_eq!(ConverseModel::ClaudeSonnet5.max_output_tokens(), 128_000);
assert_eq!(ConverseModel::ClaudeOpus4_1.max_output_tokens(), 32_000);
assert_eq!(ConverseModel::Gemma3_4B.max_output_tokens(), 8_192);

View file

@ -992,7 +992,7 @@ fn converse_language_model(model: &ConverseModel) -> LanguageModel {
supports_tools: model.supports_tool_use(),
supports_images: model.supports_images(),
supports_thinking: model.supports_thinking(),
// Astra and Fable 5.1 always reason, so only offer effort levels.
// These models always reason, so only offer effort levels.
supports_disabling_thinking: reasoning_model.is_none(),
refusal_fallback_model_id: model
.id()
@ -2058,9 +2058,13 @@ fn deny_tool_use_events(
/// Matches by Bedrock model ID, so custom entries such as
/// `us.anthropic.claude-fable-5-1` get the same handling as the built-in model.
fn converse_reasoning_model(model_id: &str) -> Option<ConverseModel> {
[ConverseModel::Gpt6Astra, ConverseModel::ClaudeFable5_1]
.into_iter()
.find(|model| model_id.ends_with(model.request_id()))
[
ConverseModel::Gpt6Astra,
ConverseModel::ClaudeFable5_1,
ConverseModel::ClaudeOpus5_5,
]
.into_iter()
.find(|model| model_id.ends_with(model.request_id()))
}
fn prepare_bedrock_content(
@ -2120,10 +2124,13 @@ pub fn into_bedrock(
}
let reasoning_model = converse_reasoning_model(&model);
let is_fable_5_1 = reasoning_model == Some(ConverseModel::ClaudeFable5_1);
let always_adaptive = matches!(
reasoning_model,
Some(ConverseModel::ClaudeFable5_1 | ConverseModel::ClaudeOpus5_5)
);
let is_gpt_6_astra = reasoning_model == Some(ConverseModel::Gpt6Astra);
if is_fable_5_1 && request.tool_choice == Some(LanguageModelToolChoice::Any) {
anyhow::bail!("Claude Fable 5.1 does not support forced tool use");
if always_adaptive && request.tool_choice == Some(LanguageModelToolChoice::Any) {
anyhow::bail!("{model} does not support forced tool use");
}
let mut new_messages: Vec<BedrockMessage> = Vec::new();
@ -2446,10 +2453,11 @@ pub fn into_bedrock(
}
let selected_effort = |default_effort| {
// Astra and Fable 5.1 keep reasoning enabled, so suppressed requests use the default
// effort, matching the Anthropic and OpenAI providers.
// Astra, Fable 5.1, and Opus 5.5 keep reasoning enabled, so suppressed requests use the
// default effort, matching the Anthropic and OpenAI providers.
// <https://developers.openai.com/api/docs/guides/latest-model?model=gpt-6-astra>
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-fable-5-1.html>
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5-5.html>
if !request.thinking_allowed {
return default_effort;
}
@ -2471,7 +2479,7 @@ pub fn into_bedrock(
effort: selected_effort(effort),
}),
BedrockModelMode::AdaptiveThinking { effort }
if request.thinking_allowed || is_fable_5_1 =>
if request.thinking_allowed || always_adaptive =>
{
Some(bedrock::Thinking::Adaptive {
effort: selected_effort(effort),
@ -2485,7 +2493,7 @@ pub fn into_bedrock(
Some(bedrock::Thinking::Enabled { budget_tokens })
}
_ if !request.thinking_allowed
&& model.contains(ConverseModel::ClaudeOpus5.request_id()) =>
&& model.ends_with(ConverseModel::ClaudeOpus5.request_id()) =>
{
// Opus 5 defaults to adaptive thinking, so turning it off requires `disabled`.
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
@ -2493,7 +2501,7 @@ pub fn into_bedrock(
}
_ => None,
};
let temperature = if is_fable_5_1 || is_gpt_6_astra {
let temperature = if always_adaptive || is_gpt_6_astra {
None
} else {
request.temperature.or(default_temperature)
@ -2541,7 +2549,7 @@ pub fn map_to_language_model_completion_events(
);
let preserve_redacted_thinking = matches!(
model,
ConverseModel::Gpt6Astra | ConverseModel::ClaudeFable5_1
ConverseModel::Gpt6Astra | ConverseModel::ClaudeFable5_1 | ConverseModel::ClaudeOpus5_5
);
let initial_state = State {
events,
@ -3487,7 +3495,7 @@ mod tests {
}
#[test]
fn test_gpt_6_astra_and_fable_5_1_defaults() -> Result<()> {
fn test_always_reasoning_model_defaults() -> Result<()> {
for (model, expected_thinking) in [
(
ConverseModel::Gpt6Astra,
@ -3499,6 +3507,12 @@ mod tests {
"effort": "High", "binding_controls_beta": "thinking-binding-controls-2026-08-01"
}}),
),
(
ConverseModel::ClaudeOpus5_5,
serde_json::json!({"Adaptive": {
"effort": "High", "binding_controls_beta": "thinking-binding-controls-2026-08-01"
}}),
),
] {
for thinking_allowed in [true, false] {
let request = into_bedrock(
@ -3644,6 +3658,73 @@ mod tests {
Ok(())
}
#[test]
fn test_opus_5_5_replays_only_its_own_reasoning() -> Result<()> {
use bedrock::bedrock_client::types::MessageStartEvent;
let response = futures::stream::iter(vec![Ok(ConverseStreamOutput::MessageStart(
MessageStartEvent::builder()
.role(bedrock::BedrockRole::Assistant)
.build()?,
))]);
let events = futures::executor::block_on(
map_to_language_model_completion_events(
Box::pin(response),
ConverseModel::ClaudeOpus5_5,
)
.collect::<Vec<_>>(),
)
.into_iter()
.collect::<Result<Vec<_>, _>>()?;
let details = events.iter().find_map(|event| match event {
LanguageModelCompletionEvent::ReasoningDetails(value) => Some(Arc::new(value.clone())),
_ => None,
});
assert!(details.is_some());
for (model, replays_reasoning) in [
(ConverseModel::ClaudeOpus5_5, true),
(ConverseModel::ClaudeFable5_1, false),
(ConverseModel::ClaudeOpus4_8, false),
] {
let request = into_bedrock(
LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::Assistant,
content: vec![
MessageContent::Thinking {
text: "thinking".into(),
signature: Some("signature".into()),
},
MessageContent::Text("answer".into()),
],
cache: false,
reasoning_details: details.clone(),
}],
..Default::default()
},
model.cross_region_inference_id("us-east-1", false)?,
model.default_temperature(),
model.max_output_tokens(),
model.thinking_mode(),
model.supports_caching(),
model.supports_tool_use(),
None,
None,
)?;
assert_eq!(
matches!(
request.messages[0].content().first(),
Some(BedrockInnerContent::ReasoningContent(_))
),
replays_reasoning,
"{}",
model.id()
);
}
Ok(())
}
#[test]
fn test_astra_parallel_tool_results_precede_their_images() -> Result<()> {
let results = (1..=2)
@ -3921,6 +4002,113 @@ mod tests {
assert_eq!(configured.temperature, Some(0.7));
}
#[test]
fn test_opus_5_5_rejects_only_forced_tool_choice() -> Result<()> {
let opus_5_5 = ConverseModel::ClaudeOpus5_5;
let custom = ConverseModel::Custom {
name: "us.anthropic.claude-opus-5-5".into(),
max_tokens: 1_000_000,
display_name: None,
max_output_tokens: Some(128_000),
default_temperature: None,
cache_configuration: None,
supports_tool_use: Some(true),
supports_images: Some(true),
thinking: None,
};
for model in [&opus_5_5, &custom] {
let language_model = converse_language_model(model);
assert!(language_model.supports_tools);
assert!(language_model.tool_choice_support.auto);
assert!(language_model.tool_choice_support.none);
assert!(
!language_model.tool_choice_support.any,
"{} rejects forced tool use",
model.id()
);
}
let forced = into_bedrock(
LanguageModelRequest {
tool_choice: Some(LanguageModelToolChoice::Any),
..Default::default()
},
opus_5_5.cross_region_inference_id("us-east-1", false)?,
opus_5_5.default_temperature(),
opus_5_5.max_output_tokens(),
opus_5_5.thinking_mode(),
opus_5_5.supports_caching(),
opus_5_5.supports_tool_use(),
None,
None,
);
assert!(forced.is_err());
for model in [
ConverseModel::ClaudeOpus5,
ConverseModel::ClaudeFable5,
ConverseModel::NovaPro,
] {
assert!(
converse_language_model(&model).tool_choice_support.any,
"{} should still support forced tool use",
model.id()
);
}
Ok(())
}
#[test]
fn test_opus_5_5_omits_sampling_controls() {
// Opus 5.5's always-on adaptive thinking rejects sampling controls, so
// its request must omit temperature (top_k/top_p are always unset).
// Other adaptive-thinking Claude models keep their default temperature.
for (model, expects_temperature) in [
("global.anthropic.claude-opus-5-5", false),
("us.anthropic.claude-opus-5-5", false),
("us.anthropic.claude-opus-5", true),
("us.anthropic.claude-sonnet-5", true),
] {
let request = into_bedrock(
LanguageModelRequest {
messages: vec![LanguageModelRequestMessage {
role: Role::User,
content: vec![MessageContent::Text("Hi".into())],
cache: false,
reasoning_details: None,
}],
..Default::default()
},
model.to_string(),
Some(1.0),
128_000,
BedrockModelMode::AdaptiveThinking {
effort: bedrock::BedrockAdaptiveThinkingEffort::High,
},
true,
true,
None,
None,
)
.unwrap();
assert_eq!(request.top_k, None, "{model} must not send top_k");
assert_eq!(request.top_p, None, "{model} must not send top_p");
if expects_temperature {
assert_eq!(
request.temperature,
Some(1.0),
"{model} should send its default temperature"
);
} else {
assert_eq!(
request.temperature, None,
"{model} must omit temperature because adaptive thinking is always on"
);
}
}
}
#[test]
fn test_cache_marked_message_that_filters_to_empty_is_dropped() {
let request = into_bedrock_request(vec![