mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-13 17:15:16 +00:00
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:
parent
efed1f245a
commit
df48a139ce
3 changed files with 298 additions and 24 deletions
|
|
@ -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!({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue