fix(providers): send explicit thinking disabled to adaptive Claude models (#11177)

This commit is contained in:
filip
2026-08-17 18:32:49 +00:00
committed by GitHub
parent 4f3bb7505a
commit 7c4ba22191
3 changed files with 69 additions and 22 deletions
@@ -98,6 +98,12 @@ fn canonical_thinking_mode(provider_name: &str, model_name: &str) -> Option<Thin
maybe_get_canonical_model(provider_name, model_name).and_then(|model| model.thinking_mode)
}
/// Adaptive models run adaptive thinking when `thinking` is omitted, so turning
/// it off takes an explicit disable. Always-on models reject that disable.
pub fn requires_explicit_thinking_disable(provider_name: &str, model_name: &str) -> bool {
canonical_thinking_mode(provider_name, model_name) == Some(ThinkingMode::Adaptive)
}
fn canonical_reasoning(provider_name: &str, model_config: &ModelConfig) -> Option<bool> {
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(())
}
@@ -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));
+30 -20
View File
@@ -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<Regex> = Lazy::new(|| Regex::new(r"-v\d+(:\d+)?$").unwrap());
pub fn bedrock_anthropic_thinking_fields(model_config: &ModelConfig) -> Option<Document> {
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<D
),
]))
}
ThinkingType::Disabled => 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<D
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;
};
fn bedrock_anthropic_model_config(model_config: &ModelConfig) -> Option<ModelConfig> {
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]