Opus 5 BYOK Support (#61596)
<img width="844" height="404" alt="Screenshot 2026-07-24 at 3 47 04 PM" src="https://github.com/user-attachments/assets/4691a246-bab9-4426-aa0b-6a7cfa2aa486" /> action support, and explicit thinking opt-out behavior. Adds Claude Opus 5 to the Anthropic and Amazon Bedrock BYOK providers, including its documented context and output limits, regional inference profiles, fast mode and comp Release Notes: - Added Claude Opus 5 support for Anthropic and Amazon Bedrock BYOK providers.
This commit is contained in:
1 parent
76e43838aa
commit
8457a4ccf4
5 files changed
+396
-80
No files matched your search
@@ -22,7 +22,22 @@ pub const ANTHROPIC_API_URL: &str = "https://api.anthropic.com";
|
||||
pub const FAST_MODE_BETA_HEADER: &str = "fast-mode-2026-02-01";
|
||||
|
||||
pub fn supports_fast_mode(model_id: &str) -> bool {
|
||||
matches!(model_id, "claude-opus-4-8")
|
||||
matches!(model_id, "claude-opus-5" | "claude-opus-4-8")
|
||||
}
|
||||
|
||||
/// Model IDs where adaptive thinking runs by default when a request omits the
|
||||
/// `thinking` field, and where thinking must instead be turned off with an
|
||||
/// explicit `thinking: {"type": "disabled"}`.
|
||||
///
|
||||
/// On earlier Opus models omitting `thinking` means thinking is off; Claude
|
||||
/// Opus 5 flipped that default. Claude Fable 5 and Claude Mythos 5 also think
|
||||
/// by default, but they reject `{"type": "disabled"}` with a 400 error, so
|
||||
/// they are deliberately excluded here (thinking cannot be turned off for
|
||||
/// them at all).
|
||||
///
|
||||
/// <https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-to-claude-opus-5>
|
||||
pub fn requires_explicit_thinking_opt_out(model_id: &str) -> bool {
|
||||
matches!(model_id, "claude-opus-5")
|
||||
}
|
||||
|
||||
pub const FABLE_MODEL_ID_PREFIX: &str = "claude-fable-5";
|
||||
@@ -168,6 +183,7 @@ impl Model {
|
||||
"claude-fable-5"
|
||||
| "claude-mythos-5"
|
||||
| "claude-mythos-preview"
|
||||
| "claude-opus-5"
|
||||
| "claude-opus-4-8"
|
||||
| "claude-opus-4-7"
|
||||
| "claude-opus-4-6"
|
||||
@@ -732,6 +748,10 @@ pub enum Thinking {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
display: Option<AdaptiveThinkingDisplay>,
|
||||
},
|
||||
/// Explicitly turns thinking off. Required by models where thinking runs
|
||||
/// by default (see [`requires_explicit_thinking_opt_out`]); only accepted
|
||||
/// at effort `high` or below.
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -1209,18 +1229,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_listed_enables_fast_mode_for_opus_4_8() {
|
||||
let model = Model::from_listed(listed_entry(
|
||||
"claude-opus-4-8",
|
||||
ModelCapabilities::default(),
|
||||
));
|
||||
fn from_listed_enables_fast_mode_and_compaction_for_supported_opus_models() {
|
||||
for model_id in ["claude-opus-5", "claude-opus-4-8"] {
|
||||
let model = Model::from_listed(listed_entry(model_id, ModelCapabilities::default()));
|
||||
|
||||
assert!(model.supports_speed);
|
||||
let beta_headers = model
|
||||
.beta_headers()
|
||||
.expect("model should have beta headers");
|
||||
assert!(beta_headers.contains(FAST_MODE_BETA_HEADER));
|
||||
assert!(beta_headers.contains(COMPACTION_BETA_HEADER));
|
||||
assert!(model.supports_speed);
|
||||
let beta_headers = model
|
||||
.beta_headers()
|
||||
.expect("model should have beta headers");
|
||||
assert!(beta_headers.contains(FAST_MODE_BETA_HEADER));
|
||||
assert!(beta_headers.contains(COMPACTION_BETA_HEADER));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -341,6 +341,29 @@ pub fn into_anthropic(
|
||||
last_tool.cache_control = Some(cache_control);
|
||||
}
|
||||
|
||||
let thinking = if request.thinking_allowed {
|
||||
match mode {
|
||||
AnthropicModelMode::Thinking { budget_tokens } => {
|
||||
Some(Thinking::Enabled { budget_tokens })
|
||||
}
|
||||
AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive {
|
||||
display: Some(AdaptiveThinkingDisplay::Summarized),
|
||||
}),
|
||||
AnthropicModelMode::Default => None,
|
||||
}
|
||||
} else if crate::requires_explicit_thinking_opt_out(&model) {
|
||||
// On Claude Opus 5, omitting the `thinking` field no longer means
|
||||
// "off": the model runs adaptive thinking by default, so features
|
||||
// that suppress thinking (e.g. inline assist) must opt out
|
||||
// explicitly. `disabled` is only accepted at effort `high` or below;
|
||||
// that holds here because `output_config` is never sent when thinking
|
||||
// is disallowed, and the server-side default effort is `high`.
|
||||
// <https://platform.claude.com/docs/en/about-claude/models/migration-guide#migrating-to-claude-opus-5>
|
||||
Some(Thinking::Disabled)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(crate::Request {
|
||||
model,
|
||||
messages: new_messages,
|
||||
@@ -356,19 +379,7 @@ pub fn into_anthropic(
|
||||
cache_type: CacheControlType::Ephemeral,
|
||||
ttl: None,
|
||||
}),
|
||||
thinking: if request.thinking_allowed {
|
||||
match mode {
|
||||
AnthropicModelMode::Thinking { budget_tokens } => {
|
||||
Some(Thinking::Enabled { budget_tokens })
|
||||
}
|
||||
AnthropicModelMode::AdaptiveThinking => Some(Thinking::Adaptive {
|
||||
display: Some(AdaptiveThinkingDisplay::Summarized),
|
||||
}),
|
||||
AnthropicModelMode::Default => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
thinking,
|
||||
tools,
|
||||
tool_choice: request.tool_choice.map(|choice| match choice {
|
||||
LanguageModelToolChoice::Auto => ToolChoice::Auto,
|
||||
@@ -952,6 +963,69 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_disallowed_sends_explicit_opt_out_only_where_required() {
|
||||
// (model, expects_explicit_opt_out): Claude Opus 5 thinks by default
|
||||
// when the `thinking` field is omitted, so suppressing thinking
|
||||
// requires sending `{"type": "disabled"}`. Earlier Opus models treat
|
||||
// omission as "off", and Fable rejects `disabled` outright, so both
|
||||
// must keep omitting the field.
|
||||
for (model, expects_explicit_opt_out) in [
|
||||
("claude-opus-5", true),
|
||||
("claude-opus-4-8", false),
|
||||
("claude-fable-5", false),
|
||||
] {
|
||||
let request = LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text("Hi".to_string())],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}],
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
intent: None,
|
||||
stop: vec![],
|
||||
temperature: None,
|
||||
tools: vec![],
|
||||
tool_choice: None,
|
||||
thinking_allowed: false,
|
||||
thinking_effort: None,
|
||||
speed: None,
|
||||
compact_at_tokens: None,
|
||||
};
|
||||
|
||||
let anthropic_request = into_anthropic(
|
||||
request,
|
||||
model.to_string(),
|
||||
1.0,
|
||||
128_000,
|
||||
AnthropicModelMode::AdaptiveThinking,
|
||||
AnthropicPromptCacheMode::Automatic,
|
||||
&ANTHROPIC_PROVIDER_ID,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if expects_explicit_opt_out {
|
||||
assert!(
|
||||
matches!(anthropic_request.thinking, Some(Thinking::Disabled)),
|
||||
"{model} should send an explicit thinking opt-out"
|
||||
);
|
||||
// `disabled` combined with effort `xhigh`/`max` is a 400, so
|
||||
// no effort may accompany the opt-out.
|
||||
assert!(
|
||||
anthropic_request.output_config.is_none(),
|
||||
"{model} must not send output_config with thinking disabled"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
anthropic_request.thinking.is_none(),
|
||||
"{model} should omit the thinking field entirely"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_cache_control_when_caching_disabled() {
|
||||
let request = LanguageModelRequest {
|
||||
|
||||
+108
-24
@@ -43,30 +43,8 @@ pub async fn stream_completion(
|
||||
|
||||
let mut additional_fields: HashMap<String, Document> = HashMap::new();
|
||||
|
||||
match request.thinking {
|
||||
Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) => {
|
||||
let thinking_config = HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
]);
|
||||
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
|
||||
}
|
||||
Some(Thinking::Adaptive { effort: _ }) => {
|
||||
let thinking_config = HashMap::from([
|
||||
("type".to_string(), Document::String("adaptive".to_string())),
|
||||
(
|
||||
"display".to_string(),
|
||||
Document::String("summarized".to_string()),
|
||||
),
|
||||
]);
|
||||
additional_fields.insert("thinking".to_string(), Document::from(thinking_config));
|
||||
}
|
||||
_ => {}
|
||||
if let Some(thinking) = &request.thinking {
|
||||
additional_fields.extend(thinking_request_fields(thinking));
|
||||
}
|
||||
|
||||
if !additional_fields.is_empty() {
|
||||
@@ -212,6 +190,64 @@ pub enum Thinking {
|
||||
Adaptive {
|
||||
effort: BedrockAdaptiveThinkingEffort,
|
||||
},
|
||||
/// Explicitly turns thinking off. Required by Claude Opus 5, where
|
||||
/// adaptive thinking runs by default when the `thinking` field is
|
||||
/// omitted; only accepted at effort `high` or below.
|
||||
///
|
||||
/// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// Converts the request's thinking configuration into the
|
||||
/// `additionalModelRequestFields` entries understood by Anthropic models on
|
||||
/// the Converse API.
|
||||
fn thinking_request_fields(thinking: &Thinking) -> HashMap<String, Document> {
|
||||
let mut fields = HashMap::new();
|
||||
match thinking {
|
||||
Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
} => {
|
||||
fields.insert(
|
||||
"thinking".to_string(),
|
||||
Document::from(HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(*budget_tokens)),
|
||||
),
|
||||
])),
|
||||
);
|
||||
}
|
||||
Thinking::Enabled {
|
||||
budget_tokens: None,
|
||||
} => {}
|
||||
Thinking::Adaptive { effort: _ } => {
|
||||
fields.insert(
|
||||
"thinking".to_string(),
|
||||
Document::from(HashMap::from([
|
||||
("type".to_string(), Document::String("adaptive".to_string())),
|
||||
(
|
||||
"display".to_string(),
|
||||
Document::String("summarized".to_string()),
|
||||
),
|
||||
])),
|
||||
);
|
||||
}
|
||||
Thinking::Disabled => {
|
||||
// On Claude Opus 5 omitting the `thinking` field means adaptive
|
||||
// thinking runs by default, so turning it off requires this
|
||||
// explicit opt-out. No effort is attached: `disabled` combined
|
||||
// with effort `xhigh`/`max` is rejected with a 400.
|
||||
fields.insert(
|
||||
"thinking".to_string(),
|
||||
Document::from(HashMap::from([(
|
||||
"type".to_string(),
|
||||
Document::String("disabled".to_string()),
|
||||
)])),
|
||||
);
|
||||
}
|
||||
}
|
||||
fields
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -255,3 +291,51 @@ pub enum BedrockError {
|
||||
#[error(transparent)]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn string_field<'a>(document: &'a Document, key: &str) -> Option<&'a str> {
|
||||
match document {
|
||||
Document::Object(map) => match map.get(key) {
|
||||
Some(Document::String(value)) => Some(value.as_str()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disabled_thinking_serializes_opt_out_without_effort() {
|
||||
let fields = thinking_request_fields(&Thinking::Disabled);
|
||||
|
||||
let thinking = fields.get("thinking").expect("thinking field");
|
||||
assert_eq!(string_field(thinking, "type"), Some("disabled"));
|
||||
// `disabled` combined with an effort of `xhigh`/`max` is a 400, so no
|
||||
// output_config may accompany the opt-out.
|
||||
assert!(!fields.contains_key("output_config"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enabled_thinking_serializes_budget_tokens() {
|
||||
let fields = thinking_request_fields(&Thinking::Enabled {
|
||||
budget_tokens: Some(4_096),
|
||||
});
|
||||
|
||||
let thinking = fields.get("thinking").expect("thinking field");
|
||||
assert_eq!(string_field(thinking, "type"), Some("enabled"));
|
||||
match thinking {
|
||||
Document::Object(map) => assert_eq!(
|
||||
map.get("budget_tokens"),
|
||||
Some(&Document::Number(AwsNumber::PosInt(4_096)))
|
||||
),
|
||||
_ => panic!("thinking field should be an object"),
|
||||
}
|
||||
|
||||
let fields = thinking_request_fields(&Thinking::Enabled {
|
||||
budget_tokens: None,
|
||||
});
|
||||
assert!(fields.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,13 @@ pub enum ConverseModel {
|
||||
alias = "claude-fable-5-thinking-latest"
|
||||
)]
|
||||
ClaudeFable5,
|
||||
#[serde(
|
||||
rename = "claude-opus-5",
|
||||
alias = "claude-opus-5-latest",
|
||||
alias = "claude-opus-5-thinking",
|
||||
alias = "claude-opus-5-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus5,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-8",
|
||||
alias = "claude-opus-4-8-latest",
|
||||
@@ -235,6 +242,8 @@ impl ConverseModel {
|
||||
pub fn from_id(id: &str) -> anyhow::Result<Self> {
|
||||
if id.starts_with("claude-fable-5") {
|
||||
Ok(Self::ClaudeFable5)
|
||||
} else if id.starts_with("claude-opus-5") {
|
||||
Ok(Self::ClaudeOpus5)
|
||||
} else if id.starts_with("claude-opus-4-8") {
|
||||
Ok(Self::ClaudeOpus4_8)
|
||||
} else if id.starts_with("claude-opus-4-7") {
|
||||
@@ -263,6 +272,7 @@ impl ConverseModel {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeFable5 => "claude-fable-5",
|
||||
Self::ClaudeOpus5 => "claude-opus-5",
|
||||
Self::ClaudeOpus4_8 => "claude-opus-4-8",
|
||||
Self::ClaudeOpus4_7 => "claude-opus-4-7",
|
||||
Self::ClaudeOpus4_6 => "claude-opus-4-6",
|
||||
@@ -316,6 +326,7 @@ impl ConverseModel {
|
||||
pub fn request_id(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeFable5 => "anthropic.claude-fable-5",
|
||||
Self::ClaudeOpus5 => "anthropic.claude-opus-5",
|
||||
Self::ClaudeOpus4_8 => "anthropic.claude-opus-4-8",
|
||||
Self::ClaudeOpus4_7 => "anthropic.claude-opus-4-7",
|
||||
Self::ClaudeOpus4_6 => "anthropic.claude-opus-4-6-v1",
|
||||
@@ -369,6 +380,7 @@ impl ConverseModel {
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeFable5 => "Claude Fable 5",
|
||||
Self::ClaudeOpus5 => "Claude Opus 5",
|
||||
Self::ClaudeOpus4_8 => "Claude Opus 4.8",
|
||||
Self::ClaudeOpus4_7 => "Claude Opus 4.7",
|
||||
Self::ClaudeOpus4_6 => "Claude Opus 4.6",
|
||||
@@ -424,6 +436,7 @@ impl ConverseModel {
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -461,6 +474,7 @@ impl ConverseModel {
|
||||
pub fn max_output_tokens(&self) -> u64 {
|
||||
match self {
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -503,6 +517,7 @@ impl ConverseModel {
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -524,6 +539,7 @@ impl ConverseModel {
|
||||
pub fn supports_tool_use(&self) -> bool {
|
||||
match self {
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -558,6 +574,7 @@ impl ConverseModel {
|
||||
pub fn supports_images(&self) -> bool {
|
||||
match self {
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -579,6 +596,7 @@ impl ConverseModel {
|
||||
pub fn supports_caching(&self) -> bool {
|
||||
match self {
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -601,6 +619,7 @@ impl ConverseModel {
|
||||
matches!(
|
||||
self,
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -618,6 +637,7 @@ impl ConverseModel {
|
||||
matches!(
|
||||
self,
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -629,7 +649,7 @@ impl ConverseModel {
|
||||
pub fn supports_xhigh_adaptive_thinking(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::ClaudeFable5 | Self::ClaudeOpus4_8 | Self::ClaudeSonnet5
|
||||
Self::ClaudeFable5 | Self::ClaudeOpus5 | Self::ClaudeOpus4_8 | Self::ClaudeSonnet5
|
||||
)
|
||||
}
|
||||
|
||||
@@ -657,6 +677,7 @@ impl ConverseModel {
|
||||
let supports_global = matches!(
|
||||
self,
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -720,6 +741,7 @@ impl ConverseModel {
|
||||
// Global inference profiles
|
||||
(
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -739,6 +761,7 @@ impl ConverseModel {
|
||||
// US region inference profiles
|
||||
(
|
||||
Self::ClaudeFable5
|
||||
| Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
@@ -763,9 +786,17 @@ impl ConverseModel {
|
||||
// Canada region inference profiles
|
||||
(Self::NovaLite, "ca") => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
// Canada has no Claude-specific `ca.` profiles. AWS instead lists
|
||||
// ca-central-1 and ca-west-1 as source regions of the US geo
|
||||
// profile for these models, which keeps data within US and Canada
|
||||
// regions. See the per-model cards, e.g.
|
||||
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>.
|
||||
(Self::ClaudeOpus5, "ca") => Ok(format!("us.{}", model_id)),
|
||||
|
||||
// EU region inference profiles
|
||||
(
|
||||
Self::ClaudeOpus4_8
|
||||
Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeSonnet4_6
|
||||
@@ -780,7 +811,8 @@ impl ConverseModel {
|
||||
|
||||
// Australia region inference profiles
|
||||
(
|
||||
Self::ClaudeOpus4_8
|
||||
Self::ClaudeOpus5
|
||||
| Self::ClaudeOpus4_8
|
||||
| Self::ClaudeOpus4_7
|
||||
| Self::ClaudeOpus4_6
|
||||
| Self::ClaudeSonnet4_6
|
||||
@@ -789,7 +821,10 @@ impl ConverseModel {
|
||||
"au",
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
// Japan region inference profiles
|
||||
// Japan region inference profiles. Claude Opus 5 is deliberately
|
||||
// absent: its model card lists only `us.`/`eu.`/`au.` geo profiles
|
||||
// (plus `global.`):
|
||||
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
|
||||
(
|
||||
Self::ClaudeSonnet4_6
|
||||
| Self::ClaudeSonnet4_5
|
||||
@@ -1083,6 +1118,10 @@ mod tests {
|
||||
ConverseModel::ClaudeSonnet5.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-sonnet-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-opus-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::NovaPro.cross_region_inference_id("us-east-2", false)?,
|
||||
"us.amazon.nova-pro-v1:0"
|
||||
@@ -1120,6 +1159,10 @@ mod tests {
|
||||
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-opus-4-8"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-opus-5"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1179,6 +1222,10 @@ mod tests {
|
||||
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"au.anthropic.claude-opus-4-8"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"au.anthropic.claude-opus-5"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1188,6 +1235,13 @@ mod tests {
|
||||
ConverseModel::ClaudeHaiku4_5.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
"jp.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
);
|
||||
// Claude Opus 5 has no `jp.` geo profile, so it falls through to the
|
||||
// bare model id (which fails at request time with a region hint)
|
||||
// rather than fabricating an undocumented profile.
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
"anthropic.claude-opus-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeSonnet4_5.cross_region_inference_id("ap-northeast-3", false)?,
|
||||
"jp.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@@ -1205,6 +1259,12 @@ mod tests {
|
||||
ConverseModel::NovaLite.cross_region_inference_id("ca-central-1", false)?,
|
||||
"ca.amazon.nova-lite-v1:0"
|
||||
);
|
||||
// Canadian regions are source regions of the US geo profile for
|
||||
// recent Opus models; there are no `ca.` Claude profiles.
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.cross_region_inference_id("ca-central-1", false)?,
|
||||
"us.anthropic.claude-opus-5"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1247,6 +1307,10 @@ mod tests {
|
||||
ConverseModel::ClaudeOpus4_8.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-opus-4-8"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-opus-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeFable5.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-fable-5"
|
||||
@@ -1327,6 +1391,7 @@ mod tests {
|
||||
assert_eq!(ConverseModel::DeepSeekR1.id(), "deepseek-r1");
|
||||
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::ClaudeSonnet5.id(), "claude-sonnet-5");
|
||||
|
||||
assert_eq!(
|
||||
@@ -1346,6 +1411,10 @@ mod tests {
|
||||
ConverseModel::ClaudeFable5.request_id(),
|
||||
"anthropic.claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeOpus5.request_id(),
|
||||
"anthropic.claude-opus-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::ClaudeSonnet5.request_id(),
|
||||
"anthropic.claude-sonnet-5"
|
||||
@@ -1365,6 +1434,12 @@ mod tests {
|
||||
.id(),
|
||||
"claude-fable-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::from_id("claude-opus-5-thinking")
|
||||
.unwrap()
|
||||
.id(),
|
||||
"claude-opus-5"
|
||||
);
|
||||
assert_eq!(
|
||||
ConverseModel::from_id("claude-sonnet-5-thinking")
|
||||
.unwrap()
|
||||
@@ -1380,14 +1455,17 @@ mod tests {
|
||||
assert!(ConverseModel::ClaudeSonnet4_5.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus4_6.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus5.supports_thinking());
|
||||
|
||||
assert!(!ConverseModel::ClaudeSonnet4.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus4_6.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet4_6.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus5.supports_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet5.supports_adaptive_thinking());
|
||||
assert!(!ConverseModel::ClaudeOpus4_7.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeFable5.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus5.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeSonnet5.supports_xhigh_adaptive_thinking());
|
||||
assert!(ConverseModel::ClaudeOpus4_8.supports_xhigh_adaptive_thinking());
|
||||
assert_eq!(BedrockAdaptiveThinkingEffort::XHigh.as_str(), "xhigh");
|
||||
@@ -1417,6 +1495,7 @@ mod tests {
|
||||
assert_eq!(ConverseModel::ClaudeSonnet4_5.max_token_count(), 1_000_000);
|
||||
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::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);
|
||||
@@ -1427,6 +1506,7 @@ mod tests {
|
||||
assert_eq!(ConverseModel::ClaudeSonnet4_5.max_output_tokens(), 64_000);
|
||||
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::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);
|
||||
|
||||
@@ -2269,39 +2269,50 @@ pub fn into_bedrock(
|
||||
}
|
||||
}
|
||||
|
||||
let thinking = if request.thinking_allowed {
|
||||
match thinking_mode {
|
||||
BedrockModelMode::Thinking { budget_tokens } => {
|
||||
Some(bedrock::Thinking::Enabled { budget_tokens })
|
||||
}
|
||||
BedrockModelMode::AdaptiveThinking {
|
||||
effort: default_effort,
|
||||
} => {
|
||||
let effort = request
|
||||
.thinking_effort
|
||||
.as_deref()
|
||||
.and_then(|e| match e {
|
||||
"low" => Some(bedrock::BedrockAdaptiveThinkingEffort::Low),
|
||||
"medium" => Some(bedrock::BedrockAdaptiveThinkingEffort::Medium),
|
||||
"high" => Some(bedrock::BedrockAdaptiveThinkingEffort::High),
|
||||
"xhigh" => Some(bedrock::BedrockAdaptiveThinkingEffort::XHigh),
|
||||
"max" => Some(bedrock::BedrockAdaptiveThinkingEffort::Max),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(default_effort);
|
||||
Some(bedrock::Thinking::Adaptive { effort })
|
||||
}
|
||||
BedrockModelMode::Default => None,
|
||||
}
|
||||
} else if model.contains(ConverseModel::ClaudeOpus5.request_id()) {
|
||||
// On Claude Opus 5, omitting the `thinking` field no longer means
|
||||
// "off": the model runs adaptive thinking by default, so features
|
||||
// that suppress thinking (e.g. inline assist) must opt out
|
||||
// explicitly. Earlier Claude models treat omission as "off" and must
|
||||
// keep omitting the field. No effort accompanies the opt-out because
|
||||
// `disabled` combined with effort `xhigh`/`max` is a 400.
|
||||
// <https://docs.aws.amazon.com/bedrock/latest/userguide/model-card-anthropic-claude-opus-5.html>
|
||||
Some(bedrock::Thinking::Disabled)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(bedrock::Request {
|
||||
model,
|
||||
messages: new_messages,
|
||||
max_tokens: max_output_tokens,
|
||||
system: system_blocks,
|
||||
tools: tool_config,
|
||||
thinking: if request.thinking_allowed {
|
||||
match thinking_mode {
|
||||
BedrockModelMode::Thinking { budget_tokens } => {
|
||||
Some(bedrock::Thinking::Enabled { budget_tokens })
|
||||
}
|
||||
BedrockModelMode::AdaptiveThinking {
|
||||
effort: default_effort,
|
||||
} => {
|
||||
let effort = request
|
||||
.thinking_effort
|
||||
.as_deref()
|
||||
.and_then(|e| match e {
|
||||
"low" => Some(bedrock::BedrockAdaptiveThinkingEffort::Low),
|
||||
"medium" => Some(bedrock::BedrockAdaptiveThinkingEffort::Medium),
|
||||
"high" => Some(bedrock::BedrockAdaptiveThinkingEffort::High),
|
||||
"xhigh" => Some(bedrock::BedrockAdaptiveThinkingEffort::XHigh),
|
||||
"max" => Some(bedrock::BedrockAdaptiveThinkingEffort::Max),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(default_effort);
|
||||
Some(bedrock::Thinking::Adaptive { effort })
|
||||
}
|
||||
BedrockModelMode::Default => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
thinking,
|
||||
metadata: None,
|
||||
stop_sequences: Vec::new(),
|
||||
temperature: request.temperature.or(Some(default_temperature)),
|
||||
@@ -2900,6 +2911,54 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_thinking_disallowed_sends_explicit_opt_out_only_on_opus_5() {
|
||||
// Claude Opus 5 runs adaptive thinking by default when the `thinking`
|
||||
// field is omitted, so suppressing thinking requires an explicit
|
||||
// `disabled` opt-out. Earlier Claude models treat omission as "off".
|
||||
for (model, expects_explicit_opt_out) in [
|
||||
("us.anthropic.claude-opus-5", true),
|
||||
("global.anthropic.claude-opus-5", true),
|
||||
("us.anthropic.claude-opus-4-8", false),
|
||||
] {
|
||||
let request = into_bedrock(
|
||||
LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![MessageContent::Text("Hi".into())],
|
||||
cache: false,
|
||||
reasoning_details: None,
|
||||
}],
|
||||
thinking_allowed: false,
|
||||
..Default::default()
|
||||
},
|
||||
model.to_string(),
|
||||
1.0,
|
||||
128_000,
|
||||
BedrockModelMode::AdaptiveThinking {
|
||||
effort: bedrock::BedrockAdaptiveThinkingEffort::High,
|
||||
},
|
||||
true,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if expects_explicit_opt_out {
|
||||
assert!(
|
||||
matches!(request.thinking, Some(bedrock::Thinking::Disabled)),
|
||||
"{model} should send an explicit thinking opt-out"
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
request.thinking.is_none(),
|
||||
"{model} should omit the thinking field entirely"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_marked_message_that_filters_to_empty_is_dropped() {
|
||||
let request = into_bedrock_request(vec![
|
||||
|
||||
Reference in new issue
Block a user