fix(providers): send explicit thinking disabled to adaptive Claude models (#11177)
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user