From 83fd44b48671c831a594d7d2e8c48f636c655363 Mon Sep 17 00:00:00 2001 From: hammadxcm Date: Wed, 17 Jun 2026 16:39:14 +0500 Subject: [PATCH 1/2] fix(anthropic): infer adaptive thinking for Claude 4.6+ outside the registry Claude Opus 4.7 and 4.8 (and Sonnet 4.6, Fable) require adaptive thinking and reject the deprecated `thinking: {type: "enabled"}` shape with a 400: "thinking.type.enabled" is not supported for this model. Use "thinking.type.adaptive" and "output_config.effort" ... The canonical model registry only carries `thinking_mode` for the first-party `anthropic/` model IDs, so the same models served through other providers (Bedrock, Vertex, Azure) or through fully custom Anthropic-compatible providers fall through to the enabled shape and 400. This is why 4.6 worked but 4.7/4.8 failed via Azure AI Foundry. Add a model-name fallback used only when the registry has no thinking_mode: Claude Opus/Sonnet 4.6 and later infer adaptive, the Fable family infers always-on adaptive, and pre-4.6 / date-suffixed legacy names are left on the enabled path. The version parse is forward-compatible, so future Claude releases (4.9, 5.x) are covered without another change. Fixes #9746 Signed-off-by: hammadxcm --- .../goose/src/providers/formats/anthropic.rs | 127 +++++++++++++++++- 1 file changed, 126 insertions(+), 1 deletion(-) diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index cc4f35ebd920..79a2b51acca4 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -76,6 +76,52 @@ fn canonical_thinking_mode(provider_name: &str, model_name: &str) -> Option Option { + let name = model_name.to_lowercase(); + if !name.contains("claude") { + return None; + } + if name.contains("fable") { + return Some(ThinkingMode::AlwaysOnAdaptive); + } + + let family = if name.contains("opus") { + "opus" + } else if name.contains("sonnet") { + "sonnet" + } else { + return None; + }; + + claude_family_version(&name, family) + .filter(|&(major, minor)| major <= 9 && (major, minor) >= (4, 6)) + .map(|_| ThinkingMode::Adaptive) +} + +/// Parse the `(major, minor)` version that follows the family token in a +/// 4.x-style Claude model name (e.g. `claude-opus-4-7`, `claude-sonnet-4.6`, +/// `claude-opus-4.8-fast`). Names that place a date after the family +/// (`claude-3-opus-20240229`) yield an implausibly large major, which callers +/// reject. Returns `None` when no version follows the family token. +fn claude_family_version(name: &str, family: &str) -> Option<(u32, u32)> { + let after = name.split_once(family)?.1.trim_start_matches(['-', '.']); + let mut parts = after.split(['-', '.']); + let major = parts.next()?.parse().ok()?; + let minor = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0); + Some((major, minor)) +} + fn canonical_reasoning(provider_name: &str, model_config: &ModelConfig) -> Option { maybe_get_canonical_model(provider_name, &model_config.model_name) .and_then(|model| model.reasoning) @@ -92,7 +138,8 @@ pub fn thinking_type(model_config: &ModelConfig) -> ThinkingType { } pub fn thinking_type_for_provider(provider_name: &str, model_config: &ModelConfig) -> ThinkingType { - let mode = canonical_thinking_mode(provider_name, &model_config.model_name); + let mode = canonical_thinking_mode(provider_name, &model_config.model_name) + .or_else(|| inferred_adaptive_thinking_mode(&model_config.model_name)); let reasoning = model_config .reasoning .or_else(|| canonical_reasoning(provider_name, model_config)); @@ -1714,6 +1761,84 @@ mod tests { assert_eq!(thinking_budget_tokens(&config), 8192); } + fn cfg_reasoning_with_effort(name: &str, effort: &str) -> ModelConfig { + let mut params = std::collections::HashMap::new(); + params.insert("thinking_effort".to_string(), json!(effort)); + ModelConfig { + model_name: name.to_string(), + request_params: Some(params), + reasoning: Some(true), + ..Default::default() + } + } + + #[test] + fn test_inferred_adaptive_thinking_mode() { + // 4.6+ Opus/Sonnet infer adaptive regardless of separator or suffix + for name in [ + "claude-opus-4-6", + "claude-opus-4-7", + "claude-opus-4-8", + "claude-opus-4.8-fast", + "claude-sonnet-4-6", + "claude-opus-4-9", + "claude-opus-5-0", + ] { + assert_eq!( + inferred_adaptive_thinking_mode(name), + Some(ThinkingMode::Adaptive), + "{name} should infer adaptive" + ); + } + assert_eq!( + inferred_adaptive_thinking_mode("claude-fable-5"), + Some(ThinkingMode::AlwaysOnAdaptive) + ); + // Pre-4.6 and date-suffixed legacy names must not infer adaptive + for name in [ + "claude-opus-4-5", + "claude-opus-4-1", + "claude-opus-4", + "claude-sonnet-4-5", + "claude-3-7-sonnet-20250219", + "claude-3-opus-20240229", + "claude-3-5-sonnet-20241022", + "gpt-4o", + ] { + assert_eq!( + inferred_adaptive_thinking_mode(name), + None, + "{name} should not infer adaptive" + ); + } + } + + #[test] + fn test_thinking_type_adaptive_for_non_registry_provider() { + let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", None::<&str>)]); + // Claude Opus 4.7/4.8 served through a provider with no canonical + // registry entry (e.g. a custom Anthropic-compatible endpoint) must + // still use adaptive thinking, not the deprecated enabled shape. + for name in ["claude-opus-4-7", "claude-opus-4-8"] { + assert_eq!( + thinking_type_for_provider( + "custom-anthropic-compatible", + &cfg_reasoning_with_effort(name, "high"), + ), + ThinkingType::Adaptive, + "{name} via custom provider should be adaptive" + ); + } + // A pre-adaptive model through the same provider still uses enabled. + assert_eq!( + thinking_type_for_provider( + "custom-anthropic-compatible", + &cfg_reasoning_with_effort("claude-opus-4-5", "high"), + ), + ThinkingType::Enabled + ); + } + #[test] fn test_thinking_type_non_claude_always_disabled() { assert_eq!( From d1ff0541d1496d48c72e195c45be4ad1d3f1746d Mon Sep 17 00:00:00 2001 From: hammadxcm Date: Sat, 20 Jun 2026 14:05:55 +0500 Subject: [PATCH 2/2] fix(anthropic): keep date-suffixed Claude 4 IDs on enabled thinking Date-suffixed Claude 4 IDs such as claude-opus-4-20250514 parsed the release date as the minor version (4, 20250514), which passed the >= (4, 6) check and wrongly inferred adaptive thinking for these pre-4.6 models. They are real in-registry IDs with no thinking_mode, so they reach this fallback. Bound the minor version (minor < 100, symmetric with major <= 9) so 8-digit dates are rejected while future 4.10+ releases still infer adaptive. Add the date-suffixed IDs as regression cases. Thanks to @kimnamu and the codex review for catching this. Signed-off-by: hammadxcm --- crates/goose/src/providers/formats/anthropic.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 79a2b51acca4..37822c9030b7 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -105,15 +105,17 @@ fn inferred_adaptive_thinking_mode(model_name: &str) -> Option { }; claude_family_version(&name, family) - .filter(|&(major, minor)| major <= 9 && (major, minor) >= (4, 6)) + .filter(|&(major, minor)| major <= 9 && minor < 100 && (major, minor) >= (4, 6)) .map(|_| ThinkingMode::Adaptive) } /// Parse the `(major, minor)` version that follows the family token in a /// 4.x-style Claude model name (e.g. `claude-opus-4-7`, `claude-sonnet-4.6`, /// `claude-opus-4.8-fast`). Names that place a date after the family -/// (`claude-3-opus-20240229`) yield an implausibly large major, which callers -/// reject. Returns `None` when no version follows the family token. +/// (`claude-3-opus-20240229`) yield an implausibly large major, and +/// date-suffixed Claude 4 IDs (`claude-opus-4-20250514`) yield an implausibly +/// large minor; callers reject both with `major <= 9 && minor < 100`. Returns +/// `None` when no version follows the family token. fn claude_family_version(name: &str, family: &str) -> Option<(u32, u32)> { let after = name.split_once(family)?.1.trim_start_matches(['-', '.']); let mut parts = after.split(['-', '.']); @@ -1782,6 +1784,7 @@ mod tests { "claude-opus-4.8-fast", "claude-sonnet-4-6", "claude-opus-4-9", + "claude-opus-4-10", "claude-opus-5-0", ] { assert_eq!( @@ -1800,6 +1803,10 @@ mod tests { "claude-opus-4-1", "claude-opus-4", "claude-sonnet-4-5", + "claude-opus-4-20250514", + "claude-sonnet-4-20250514", + "us.anthropic.claude-sonnet-4-20250514-v1:0", + "claude-opus-4-1-20250805", "claude-3-7-sonnet-20250219", "claude-3-opus-20240229", "claude-3-5-sonnet-20241022",