Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use goose::config::{
use goose::model::ModelConfig;
use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY};
use goose::providers::base::ConfigKey;
use goose::providers::formats::anthropic::supports_adaptive_thinking;
use goose::providers::provider_test::test_provider_configuration;
use goose::providers::{create, providers, retry_operation, RetryConfig};
use goose::session::SessionType;
Expand Down Expand Up @@ -765,6 +766,53 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
config.set_gemini3_thinking_level(thinking_level)?;
}

if model.to_lowercase().starts_with("claude-") {
let supports_adaptive = supports_adaptive_thinking(&model);

let mut thinking_select = cliclack::select("Select extended thinking mode for Claude:");
if supports_adaptive {
thinking_select = thinking_select.item(
"adaptive",
"Adaptive - Claude decides when and how much to think (recommended)",
"",
);
}
thinking_select = thinking_select
.item("enabled", "Enabled - Fixed token budget for thinking", "")
.item("disabled", "Disabled - No extended thinking", "");
if supports_adaptive {
thinking_select = thinking_select.initial_value("adaptive");
} else {
thinking_select = thinking_select.initial_value("disabled");
}
let thinking_type: &str = thinking_select.interact()?;
config.set_claude_thinking_type(thinking_type)?;

if thinking_type == "adaptive" {
let effort: &str = cliclack::select("Select adaptive thinking effort level:")
.item("low", "Low - Minimal thinking, fastest responses", "")
.item("medium", "Medium - Moderate thinking", "")
.item("high", "High - Deep reasoning (default)", "")
.item(
"max",
"Max - No constraints on thinking depth (Opus 4.6 only)",
"",
)
.initial_value("high")
.interact()?;
config.set_claude_thinking_effort(effort)?;
} else if thinking_type == "enabled" {
let budget: String = cliclack::input("Enter thinking budget (tokens):")
.default_input("16000")
.validate(|input: &String| match input.parse::<i32>() {
Ok(n) if n > 0 => Ok(()),
_ => Err("Please enter a valid positive number"),
})
.interact()?;
config.set_claude_thinking_budget(budget.parse::<i32>()?)?;
}
}

// Test the configuration
let spin = spinner();
spin.start("Checking your configuration...");
Expand Down
3 changes: 3 additions & 0 deletions crates/goose/src/config/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,9 @@ config_value!(GOOSE_PROMPT_EDITOR, Option<String>);
config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize);
config_value!(GOOSE_DISABLE_SESSION_NAMING, bool);
config_value!(GEMINI3_THINKING_LEVEL, String);
config_value!(CLAUDE_THINKING_TYPE, String);
config_value!(CLAUDE_THINKING_EFFORT, String);
config_value!(CLAUDE_THINKING_BUDGET, i32);

/// Load init-config.yaml from workspace root if it exists.
/// This function is shared between the config recovery and the init_config endpoint.
Expand Down
54 changes: 53 additions & 1 deletion crates/goose/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub enum ConfigError {
InvalidRange(String, String),
}

#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
pub struct ModelConfig {
pub model_name: String,
pub context_limit: Option<usize>,
Expand Down Expand Up @@ -284,6 +284,22 @@ impl ModelConfig {
4_096
}

pub fn get_config_param<T: for<'de> serde::Deserialize<'de>>(
&self,
request_key: &str,
config_key: &str,
) -> Option<T> {
self.request_params
.as_ref()
.and_then(|params| params.get(request_key))
.and_then(|v| serde_json::from_value(v.clone()).ok())
.or_else(|| {
crate::config::Config::global()
.get_param::<T>(config_key)
.ok()
})
}

pub fn new_or_fail(model_name: &str) -> ModelConfig {
ModelConfig::new(model_name)
.unwrap_or_else(|_| panic!("Failed to create model config for {}", model_name))
Expand Down Expand Up @@ -357,4 +373,40 @@ mod tests {
let config = ModelConfig::new("test-model").unwrap();
assert_eq!(config.max_tokens, None);
}

#[test]
fn test_get_config_param() {
let _guard = env_lock::lock_env([
("CLAUDE_THINKING_EFFORT", Some("high")),
("CLAUDE_THINKING_TYPE", None::<&str>),
]);

let mut params = HashMap::new();
params.insert("effort".to_string(), serde_json::json!("low"));

let config_with_params = ModelConfig {
model_name: "test".to_string(),
request_params: Some(params),
..Default::default()
};

let config_without_params = ModelConfig {
request_params: None,
..config_with_params.clone()
};

assert_eq!(
config_with_params.get_config_param::<String>("effort", "CLAUDE_THINKING_EFFORT"),
Some("low".to_string())
);
assert_eq!(
config_without_params.get_config_param::<String>("effort", "CLAUDE_THINKING_EFFORT"),
Some("high".to_string())
);
assert_eq!(
config_without_params
.get_config_param::<String>("nonexistent", "NONEXISTENT_CONFIG_KEY"),
None
);
}
}
10 changes: 7 additions & 3 deletions crates/goose/src/providers/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ use tokio_util::io::StreamReader;
use super::api_client::{ApiClient, AuthMethod};
use super::base::{ConfigKey, MessageStream, ModelInfo, Provider, ProviderDef, ProviderMetadata};
use super::errors::ProviderError;
use super::formats::anthropic::{create_request, response_to_streaming_message};
use super::formats::anthropic::{
create_request, response_to_streaming_message, thinking_type, ThinkingType,
};
use super::openai_compatible::handle_status_openai_compat;
use super::openai_compatible::map_http_error_to_provider_error;
use crate::config::declarative_providers::DeclarativeProviderConfig;
Expand All @@ -25,6 +27,9 @@ const ANTHROPIC_PROVIDER_NAME: &str = "anthropic";
pub const ANTHROPIC_DEFAULT_MODEL: &str = "claude-sonnet-4-5";
const ANTHROPIC_DEFAULT_FAST_MODEL: &str = "claude-haiku-4-5";
const ANTHROPIC_KNOWN_MODELS: &[&str] = &[
// Claude 4.6 models
"claude-opus-4-6",
"claude-sonnet-4-6",
// Claude 4.5 models with aliases
"claude-sonnet-4-5",
"claude-sonnet-4-5-20250929",
Expand Down Expand Up @@ -124,9 +129,8 @@ impl AnthropicProvider {
fn get_conditional_headers(&self) -> Vec<(&str, &str)> {
let mut headers = Vec::new();

let is_thinking_enabled = std::env::var("CLAUDE_THINKING_ENABLED").is_ok();
if self.model.model_name.starts_with("claude-3-7-sonnet-") {
if is_thinking_enabled {
if thinking_type(&self.model) == ThinkingType::Enabled {
headers.push(("anthropic-beta", "output-128k-2025-02-19"));
}
headers.push(("anthropic-beta", "token-efficient-tools-2025-02-19"));
Expand Down
Loading