From 6d7e2da8c4b8cdb1c66f4aa8bf14e4cb929d0d52 Mon Sep 17 00:00:00 2001 From: filip <44206832+filipkujawa@users.noreply.github.com> Date: Wed, 24 Jun 2026 12:40:50 -0700 Subject: [PATCH] perf: disable extended thinking for fast-model operations (#9815) Signed-off-by: Filip Kujawa Co-authored-by: Douwe M Osinga --- crates/goose/src/model_config.rs | 9 +++- crates/goose/src/providers/anthropic.rs | 1 + .../goose/src/providers/formats/anthropic.rs | 45 ++++++++++++------- crates/goose/src/providers/formats/google.rs | 31 +++++++++++++ .../goose/src/providers/formats/openrouter.rs | 19 ++++++++ 5 files changed, 87 insertions(+), 18 deletions(-) diff --git a/crates/goose/src/model_config.rs b/crates/goose/src/model_config.rs index 2e616de76..a66df05ef 100644 --- a/crates/goose/src/model_config.rs +++ b/crates/goose/src/model_config.rs @@ -5,6 +5,7 @@ use anyhow::{anyhow, Result}; use goose_providers::conversation::token_usage::ProviderUsage; use goose_providers::errors::ProviderError; use goose_providers::model::ModelConfig; +use goose_providers::thinking::ThinkingEffort; use rmcp::model::Tool; use serde_json::Value; use std::collections::HashMap; @@ -110,7 +111,8 @@ pub async fn complete_fast( ) -> Result<(Message, ProviderUsage), ProviderError> { let fast_model_config = get_fast_model(provider.get_name(), model_config) .await - .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; + .map_err(|e| ProviderError::ExecutionError(e.to_string()))? + .with_thinking_effort(ThinkingEffort::Off); match provider .complete(&fast_model_config, session_id, system, messages, tools) @@ -124,8 +126,11 @@ pub async fn complete_fast( e, model_config.model_name ); + let fallback_config = model_config + .clone() + .with_thinking_effort(ThinkingEffort::Off); provider - .complete(model_config, session_id, system, messages, tools) + .complete(&fallback_config, session_id, system, messages, tools) .await } Err(e) => Err(e), diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index 80379a444..e913fbaeb 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -164,6 +164,7 @@ impl AnthropicProvider { AnthropicFormatOptions { preserve_unsigned_thinking: preserves_thinking, preserve_thinking_context: preserves_thinking, + thinking_disabled: false, } } diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 5e3d2b3ea..750588825 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -47,6 +47,7 @@ string_enum!(ThinkingType { Adaptive => "adaptive", Enabled => "enabled", Disabl pub struct AnthropicFormatOptions { pub preserve_unsigned_thinking: bool, pub preserve_thinking_context: bool, + pub thinking_disabled: bool, } impl AnthropicFormatOptions { @@ -68,10 +69,13 @@ impl AnthropicFormatOptions { }) .unwrap_or(self.preserve_unsigned_thinking) || preserve_thinking_context; + let thinking_disabled = model_config.reasoning == Some(false) + || model_config.thinking_effort() == Some(ThinkingEffort::Off); Self { preserve_unsigned_thinking, preserve_thinking_context, + thinking_disabled, } } } @@ -257,24 +261,31 @@ fn format_messages_with_options( // Skip } MessageContent::Thinking(thinking) => { - if !thinking.signature.is_empty() { - content.push(json!({ - TYPE_FIELD: THINKING_TYPE, - THINKING_TYPE: thinking.thinking, - SIGNATURE_FIELD: thinking.signature - })); - } else if options.preserve_unsigned_thinking && !thinking.thinking.is_empty() { - content.push(json!({ - TYPE_FIELD: THINKING_TYPE, - THINKING_TYPE: thinking.thinking - })); + // Anthropic rejects thinking blocks sent without a matching thinking config. + if !options.thinking_disabled { + if !thinking.signature.is_empty() { + content.push(json!({ + TYPE_FIELD: THINKING_TYPE, + THINKING_TYPE: thinking.thinking, + SIGNATURE_FIELD: thinking.signature + })); + } else if options.preserve_unsigned_thinking + && !thinking.thinking.is_empty() + { + content.push(json!({ + TYPE_FIELD: THINKING_TYPE, + THINKING_TYPE: thinking.thinking + })); + } } } MessageContent::RedactedThinking(redacted) => { - content.push(json!({ - TYPE_FIELD: REDACTED_THINKING_TYPE, - DATA_FIELD: redacted.data - })); + if !options.thinking_disabled { + content.push(json!({ + TYPE_FIELD: REDACTED_THINKING_TYPE, + DATA_FIELD: redacted.data + })); + } } MessageContent::Image(image) => { content.push(convert_image(image, &ImageFormat::Anthropic)); @@ -600,7 +611,7 @@ fn apply_thinking_config( ThinkingType::Disabled => {} } - if options.preserve_thinking_context { + if options.preserve_thinking_context && !options.thinking_disabled { if !obj.contains_key("thinking") { let budget_tokens = thinking_budget_tokens(model_config) .min(max_tokens.saturating_sub(MIN_ANSWER_TOKENS)); @@ -1195,6 +1206,7 @@ mod tests { AnthropicFormatOptions { preserve_unsigned_thinking: true, preserve_thinking_context: false, + thinking_disabled: false, }, ); @@ -1409,6 +1421,7 @@ mod tests { AnthropicFormatOptions { preserve_unsigned_thinking: true, preserve_thinking_context: true, + thinking_disabled: false, }, )?; diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index 3c3387b97..b8a5e2770 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -538,6 +538,24 @@ struct GoogleRequest<'a> { } fn get_thinking_config(model_config: &ModelConfig) -> Option { + if model_config.reasoning == Some(false) + || model_config.thinking_effort() == Some(ThinkingEffort::Off) + { + // Gemini 2.5 Flash defaults to dynamic thinking; only an explicit budget + // of 0 turns it off. Other families can't be disabled, so leave them unset. + if model_config + .model_name + .to_lowercase() + .starts_with("gemini-2.5-flash") + { + return Some(ThinkingConfig { + thinking_level: None, + thinking_budget: Some(0), + include_thoughts: false, + }); + } + return None; + } let model_name = model_config.model_name.to_lowercase(); let is_gemini_3 = model_name.starts_with("gemini-3"); let is_gemini_25 = model_name.starts_with("gemini-2.5"); @@ -1397,6 +1415,19 @@ data: [DONE]"#; assert!(schema.get("$defs").is_some()); } + #[test] + fn test_get_thinking_config_disabled_reasoning() { + use goose_providers::model::ModelConfig; + + let config = ModelConfig::new("gemini-2.5-flash").with_thinking_effort(ThinkingEffort::Off); + let thinking_config = get_thinking_config(&config).unwrap(); + assert_eq!(thinking_config.thinking_budget, Some(0)); + assert!(!thinking_config.include_thoughts); + + let config = ModelConfig::new("gemini-2.5-pro").with_thinking_effort(ThinkingEffort::Off); + assert!(get_thinking_config(&config).is_none()); + } + #[test] fn test_get_thinking_config() { use goose_providers::model::ModelConfig; diff --git a/crates/goose/src/providers/formats/openrouter.rs b/crates/goose/src/providers/formats/openrouter.rs index 20cbda455..ff7ac35b3 100644 --- a/crates/goose/src/providers/formats/openrouter.rs +++ b/crates/goose/src/providers/formats/openrouter.rs @@ -201,6 +201,25 @@ mod tests { assert!(payload.get("reasoning_effort").is_none()); } + #[test] + fn test_apply_reasoning_config_disables_reasoning_capable_model() { + let mut payload = json!({ + "model": "google/gemini-2.5-flash", + "messages": [] + }); + // Reasoning-capable model (per canonical) with thinking explicitly off, as a + // fast-model config is built: OpenRouter must still emit the disable object. + let mut model_config = ModelConfig::new("google/gemini-2.5-flash"); + model_config.reasoning = Some(true); + let mut params = HashMap::new(); + params.insert("thinking_effort".to_string(), json!("off")); + model_config.request_params = Some(params); + + apply_reasoning_config(&mut payload, &model_config); + + assert_eq!(payload["reasoning"], json!({ "effort": "none" })); + } + #[test] fn test_apply_reasoning_config_uses_reasoning_metadata() { let mut payload = json!({