perf: disable extended thinking for fast-model operations (#9815)
Signed-off-by: Filip Kujawa <filip.j.kujawa@gmail.com> Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -164,6 +164,7 @@ impl AnthropicProvider {
|
||||
AnthropicFormatOptions {
|
||||
preserve_unsigned_thinking: preserves_thinking,
|
||||
preserve_thinking_context: preserves_thinking,
|
||||
thinking_disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)?;
|
||||
|
||||
|
||||
@@ -538,6 +538,24 @@ struct GoogleRequest<'a> {
|
||||
}
|
||||
|
||||
fn get_thinking_config(model_config: &ModelConfig) -> Option<ThinkingConfig> {
|
||||
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;
|
||||
|
||||
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user