From c453d2878228c3a6291edf618200ba794db00df9 Mon Sep 17 00:00:00 2001 From: rabi Date: Thu, 19 Feb 2026 16:46:30 +0530 Subject: [PATCH] feat: support Anthropic adaptive thinking Add adaptive thinking support for Claude 4.6 models (Opus, Sonnet) with three thinking modes: adaptive, enabled (budget-based), and disabled. Change-Id: I33dbf77bf50ad78e909b3600705b706999bf1ebf Signed-off-by: rabi --- crates/goose-cli/src/commands/configure.rs | 48 ++++ crates/goose/src/config/base.rs | 3 + crates/goose/src/model.rs | 54 +++- crates/goose/src/providers/anthropic.rs | 10 +- .../goose/src/providers/formats/anthropic.rs | 257 ++++++++++++++++-- .../src/providers/formats/gcpvertexai.rs | 2 + crates/goose/src/providers/formats/google.rs | 11 +- .../models/subcomponents/SwitchModelModal.tsx | 125 ++++++++- 8 files changed, 476 insertions(+), 34 deletions(-) diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 02e977506bed..f1da176264c3 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -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; @@ -765,6 +766,53 @@ pub async fn configure_provider_dialog() -> anyhow::Result { 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::() { + Ok(n) if n > 0 => Ok(()), + _ => Err("Please enter a valid positive number"), + }) + .interact()?; + config.set_claude_thinking_budget(budget.parse::()?)?; + } + } + // Test the configuration let spin = spinner(); spin.start("Checking your configuration..."); diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index ae3d96c04663..0d18c7b7d1f0 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -977,6 +977,9 @@ config_value!(GOOSE_PROMPT_EDITOR, Option); 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. diff --git a/crates/goose/src/model.rs b/crates/goose/src/model.rs index 32fb80d18398..5842884035f9 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose/src/model.rs @@ -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, @@ -284,6 +284,22 @@ impl ModelConfig { 4_096 } + pub fn get_config_param serde::Deserialize<'de>>( + &self, + request_key: &str, + config_key: &str, + ) -> Option { + 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::(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)) @@ -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::("effort", "CLAUDE_THINKING_EFFORT"), + Some("low".to_string()) + ); + assert_eq!( + config_without_params.get_config_param::("effort", "CLAUDE_THINKING_EFFORT"), + Some("high".to_string()) + ); + assert_eq!( + config_without_params + .get_config_param::("nonexistent", "NONEXISTENT_CONFIG_KEY"), + None + ); + } } diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index 24eb5fcab0a2..5e6613a57ec2 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -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; @@ -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", @@ -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")); diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 9f7ab7898477..930d54d96038 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -8,8 +8,78 @@ use rmcp::model::{object, CallToolRequestParams, ErrorCode, ErrorData, JsonObjec use rmcp::object as json_object; use serde_json::{json, Value}; use std::collections::HashSet; +use std::fmt; +use std::str::FromStr; use std::sync::Arc; +macro_rules! string_enum { + ($name:ident { $($variant:ident => $str:literal),+ $(,)? }) => { + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum $name { $($variant),+ } + + impl FromStr for $name { + type Err = String; + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + $($str => Ok(Self::$variant),)+ + other => Err(format!("unknown {}: '{other}'", stringify!($name))), + } + } + } + + impl fmt::Display for $name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { $(Self::$variant => write!(f, $str),)+ } + } + } + } +} + +string_enum!(ThinkingType { Adaptive => "adaptive", Enabled => "enabled", Disabled => "disabled" }); +string_enum!(ThinkingEffort { Low => "low", Medium => "medium", High => "high", Max => "max" }); + +pub fn supports_adaptive_thinking(model_name: &str) -> bool { + let lower = model_name.to_lowercase(); + lower.contains("claude-opus-4-6") || lower.contains("claude-sonnet-4-6") +} + +pub fn thinking_type(model_config: &ModelConfig) -> ThinkingType { + let model_lower = model_config.model_name.to_lowercase(); + if !model_lower.contains("claude") { + return ThinkingType::Disabled; + } + + let is_adaptive_model = supports_adaptive_thinking(&model_config.model_name); + + if let Some(s) = + model_config.get_config_param::("thinking_type", "CLAUDE_THINKING_TYPE") + { + let tt = s.parse::().unwrap_or_else(|e| { + tracing::warn!("{e}"); + ThinkingType::Disabled + }); + if tt == ThinkingType::Adaptive && !is_adaptive_model { + tracing::warn!( + "Adaptive thinking not supported for {}, disabling thinking", + model_config.model_name + ); + return ThinkingType::Disabled; + } + return tt; + } + + if is_adaptive_model { + ThinkingType::Adaptive + } else if std::env::var("CLAUDE_THINKING_ENABLED").is_ok() { + tracing::warn!( + "CLAUDE_THINKING_ENABLED is deprecated, use CLAUDE_THINKING_TYPE=enabled instead" + ); + ThinkingType::Enabled + } else { + ThinkingType::Disabled + } +} + // Constants for frequently used strings in Anthropic API format const TYPE_FIELD: &str = "type"; const CONTENT_FIELD: &str = "content"; @@ -386,6 +456,42 @@ pub fn get_usage(data: &Value) -> Result { } } +pub fn thinking_effort(model_config: &ModelConfig) -> ThinkingEffort { + match model_config.get_config_param::("effort", "CLAUDE_THINKING_EFFORT") { + Some(s) => s.parse().unwrap_or_else(|e| { + tracing::warn!("{e}, defaulting to 'high'"); + ThinkingEffort::High + }), + None => ThinkingEffort::High, + } +} + +fn apply_thinking_config(payload: &mut Value, model_config: &ModelConfig, max_tokens: i32) { + let obj = payload.as_object_mut().unwrap(); + match thinking_type(model_config) { + ThinkingType::Adaptive => { + obj.insert("thinking".to_string(), json!({"type": "adaptive"})); + let effort = thinking_effort(model_config).to_string(); + obj.insert("output_config".to_string(), json!({"effort": effort})); + } + ThinkingType::Enabled => { + let budget_tokens = model_config + .get_config_param::("budget_tokens", "CLAUDE_THINKING_BUDGET") + .unwrap_or(16000); + + obj.insert("max_tokens".to_string(), json!(max_tokens + budget_tokens)); + obj.insert( + "thinking".to_string(), + json!({ + "type": "enabled", + "budget_tokens": budget_tokens + }), + ); + } + ThinkingType::Disabled => {} + } +} + /// Create a complete request payload for Anthropic's API pub fn create_request( model_config: &ModelConfig, @@ -429,26 +535,8 @@ pub fn create_request( .insert("temperature".to_string(), json!(temp)); } - let is_thinking_enabled = std::env::var("CLAUDE_THINKING_ENABLED").is_ok(); - if is_thinking_enabled { - let budget_tokens = std::env::var("CLAUDE_THINKING_BUDGET") - .unwrap_or_else(|_| "16000".to_string()) - .parse() - .unwrap_or(16000); + apply_thinking_config(&mut payload, model_config, max_tokens); - payload - .as_object_mut() - .unwrap() - .insert("max_tokens".to_string(), json!(max_tokens + budget_tokens)); - - payload.as_object_mut().unwrap().insert( - "thinking".to_string(), - json!({ - "type": "enabled", - "budget_tokens": budget_tokens - }), - ); - } Ok(payload) } @@ -698,6 +786,7 @@ where mod tests { use super::*; use crate::conversation::message::Message; + use crate::model::ModelConfig; use rmcp::object; use serde_json::json; @@ -964,6 +1053,70 @@ mod tests { Ok(()) } + #[test] + fn test_create_request_adaptive_thinking_for_46_models() -> Result<()> { + let _guard = env_lock::lock_env([ + ("CLAUDE_THINKING_TYPE", Some("adaptive")), + ("CLAUDE_THINKING_EFFORT", Some("high")), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ]); + + let mut config = cfg("claude-opus-4-6"); + config.max_tokens = Some(4096); + let messages = vec![Message::user().with_text("Hello")]; + let payload = create_request(&config, "system", &messages, &[])?; + + assert_eq!(payload["thinking"]["type"], "adaptive"); + assert_eq!(payload["output_config"]["effort"], "high"); + assert!(payload.get("budget_tokens").is_none()); + + Ok(()) + } + + #[test] + fn test_create_request_enabled_thinking_with_budget() -> Result<()> { + let _guard = env_lock::lock_env([ + ("CLAUDE_THINKING_TYPE", None::<&str>), + ("CLAUDE_THINKING_EFFORT", None::<&str>), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ("CLAUDE_THINKING_BUDGET", None::<&str>), + ]); + + let mut params = std::collections::HashMap::new(); + params.insert("thinking_type".to_string(), json!("enabled")); + params.insert("budget_tokens".to_string(), json!(10000)); + + let mut config = cfg("claude-3-7-sonnet-20250219"); + config.max_tokens = Some(4096); + config.request_params = Some(params); + + let messages = vec![Message::user().with_text("Hello")]; + let payload = create_request(&config, "system", &messages, &[])?; + + assert_eq!(payload["thinking"]["type"], "enabled"); + assert_eq!(payload["thinking"]["budget_tokens"], 10000); + assert_eq!(payload["max_tokens"], 4096 + 10000); + + Ok(()) + } + + #[test] + fn test_create_request_disabled_thinking_no_thinking_field() -> Result<()> { + let _guard = env_lock::lock_env([ + ("CLAUDE_THINKING_TYPE", None::<&str>), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ]); + + let config = cfg("claude-sonnet-4-20250514"); + let messages = vec![Message::user().with_text("Hello")]; + let payload = create_request(&config, "system", &messages, &[])?; + + assert!(payload.get("thinking").is_none()); + assert!(payload.get("output_config").is_none()); + + Ok(()) + } + #[test] fn test_tool_error_handling_maintains_pairing() { use crate::conversation::message::Message; @@ -1040,4 +1193,70 @@ mod tests { assert_eq!(assistant_content.len(), 1); assert_eq!(assistant_content[0]["type"], "tool_use"); } + + fn cfg(name: &str) -> ModelConfig { + ModelConfig { + model_name: name.to_string(), + ..Default::default() + } + } + + fn cfg_with_thinking(name: &str, tt: &str) -> ModelConfig { + let mut params = std::collections::HashMap::new(); + params.insert("thinking_type".to_string(), json!(tt)); + ModelConfig { + model_name: name.to_string(), + request_params: Some(params), + ..Default::default() + } + } + + #[test] + fn test_thinking_type_explicit_params() { + assert_eq!( + thinking_type(&cfg_with_thinking("claude-opus-4-6", "adaptive")), + ThinkingType::Adaptive + ); + assert_eq!( + thinking_type(&cfg_with_thinking("claude-opus-4-6", "disabled")), + ThinkingType::Disabled + ); + assert_eq!( + thinking_type(&cfg_with_thinking("claude-3-7-sonnet-20250219", "enabled")), + ThinkingType::Enabled + ); + assert_eq!( + thinking_type(&cfg_with_thinking("claude-3-7-sonnet-20250219", "adaptive")), + ThinkingType::Disabled + ); + assert_eq!( + thinking_type(&cfg_with_thinking("claude-opus-4-6", "adapttive")), + ThinkingType::Disabled + ); + } + + #[test] + fn test_thinking_type_non_claude_always_disabled() { + assert_eq!(thinking_type(&cfg("gpt-4o")), ThinkingType::Disabled); + assert_eq!( + thinking_type(&cfg_with_thinking("gpt-4o", "enabled")), + ThinkingType::Disabled + ); + } + + #[test] + fn test_thinking_type_env_var_override() { + let _guard = env_lock::lock_env([ + ("CLAUDE_THINKING_TYPE", Some("adaptive")), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ]); + assert_eq!( + thinking_type(&cfg("claude-opus-4-6")), + ThinkingType::Adaptive + ); + assert_eq!( + thinking_type(&cfg("claude-3-7-sonnet-20250219")), + ThinkingType::Disabled + ); + } } diff --git a/crates/goose/src/providers/formats/gcpvertexai.rs b/crates/goose/src/providers/formats/gcpvertexai.rs index 1a90e6347955..c4ae9979e4e8 100644 --- a/crates/goose/src/providers/formats/gcpvertexai.rs +++ b/crates/goose/src/providers/formats/gcpvertexai.rs @@ -65,6 +65,8 @@ pub enum ModelError { pub const DEFAULT_MODEL: &str = "gemini-2.5-flash"; pub const KNOWN_MODELS: &[&str] = &[ + "claude-opus-4-6", + "claude-sonnet-4-6", "claude-opus-4-5@20251101", "claude-sonnet-4-5@20250929", "claude-opus-4-1@20250805", diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index e9deda62b0a0..c2f1bd8ed678 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -547,17 +547,8 @@ fn get_thinking_config(model_config: &ModelConfig) -> Option { } let thinking_level_str = model_config - .request_params - .as_ref() - .and_then(|params| params.get("thinking_level")) - .and_then(|v| v.as_str()) + .get_config_param::("thinking_level", "GEMINI3_THINKING_LEVEL") .map(|s| s.to_lowercase()) - .or_else(|| { - crate::config::Config::global() - .get_param::("gemini3_thinking_level") - .ok() - .map(|s| s.to_lowercase()) - }) .unwrap_or_else(|| "low".to_string()); let thinking_level = match thinking_level_str.as_str() { diff --git a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx index 6d294a18b9ee..4833f7ce1c03 100644 --- a/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx +++ b/ui/desktop/src/components/settings/models/subcomponents/SwitchModelModal.tsx @@ -26,6 +26,22 @@ const THINKING_LEVEL_OPTIONS = [ { value: 'high', label: 'High - Deeper reasoning, higher latency' }, ]; +const CLAUDE_THINKING_EFFORT_OPTIONS = [ + { value: 'low', label: 'Low - Minimal thinking, fastest responses' }, + { value: 'medium', label: 'Medium - Moderate thinking' }, + { value: 'high', label: 'High - Deep reasoning (default)' }, + { value: 'max', label: 'Max - No constraints on thinking depth' }, +]; + +function isClaudeModel(name: string | null | undefined): boolean { + return !!name && name.toLowerCase().startsWith('claude-'); +} + +function supportsAdaptiveThinking(name: string): boolean { + const lower = name.toLowerCase(); + return lower.includes('claude-opus-4-6') || lower.includes('claude-sonnet-4-6'); +} + const PREFERRED_MODEL_PATTERNS = [ /claude-sonnet-4/i, /claude-4/i, @@ -80,7 +96,7 @@ export const SwitchModelModal = ({ initialProvider, titleOverride, }: SwitchModelModalProps) => { - const { getProviders, read } = useConfig(); + const { getProviders, read, upsert } = useConfig(); const { changeModel, currentModel, currentProvider } = useModelAndProvider(); const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]); type ModelOption = { value: string; label: string; provider: string; isDisabled?: boolean }; @@ -103,9 +119,41 @@ export const SwitchModelModal = ({ const [userClearedModel, setUserClearedModel] = useState(false); const [providerErrors, setProviderErrors] = useState>({}); const [thinkingLevel, setThinkingLevel] = useState('low'); + const [claudeThinkingType, setClaudeThinkingType] = useState('disabled'); + const [claudeThinkingEffort, setClaudeThinkingEffort] = useState('high'); + const [claudeThinkingBudget, setClaudeThinkingBudget] = useState('16000'); const modelName = usePredefinedModels ? selectedPredefinedModel?.name : model; const isGemini3Model = modelName?.toLowerCase().startsWith('gemini-3') ?? false; + const showClaudeThinking = isClaudeModel(modelName); + const modelSupportsAdaptive = modelName ? supportsAdaptiveThinking(modelName) : false; + + useEffect(() => { + if (!showClaudeThinking) return; + if (claudeThinkingType === 'adaptive' && !modelSupportsAdaptive) { + setClaudeThinkingType('disabled'); + } + }, [modelName, showClaudeThinking, modelSupportsAdaptive, claudeThinkingType]); + + useEffect(() => { + const readConfig = async (key: string): Promise => { + try { + const val = (await read(key, false)) as string; + return val || null; + } catch (e) { + console.warn(`Could not read ${key}, using default:`, e); + return null; + } + }; + (async () => { + const tt = await readConfig('CLAUDE_THINKING_TYPE'); + if (tt) setClaudeThinkingType(tt); + const effort = await readConfig('CLAUDE_THINKING_EFFORT'); + if (effort) setClaudeThinkingEffort(effort); + const budget = await readConfig('CLAUDE_THINKING_BUDGET'); + if (budget) setClaudeThinkingBudget(budget); + })(); + }, [read]); // Validate form data const validateForm = useCallback(() => { @@ -167,6 +215,26 @@ export const SwitchModelModal = ({ }; } + if (showClaudeThinking) { + const params: Record = { + ...modelObj.request_params, + thinking_type: claudeThinkingType, + }; + if (claudeThinkingType === 'adaptive') { + params.effort = claudeThinkingEffort; + } else if (claudeThinkingType === 'enabled') { + params.budget_tokens = parseInt(claudeThinkingBudget, 10) || 16000; + } + modelObj = { ...modelObj, request_params: params }; + + upsert('CLAUDE_THINKING_TYPE', claudeThinkingType, false).catch(console.warn); + if (claudeThinkingType === 'adaptive') { + upsert('CLAUDE_THINKING_EFFORT', claudeThinkingEffort, false).catch(console.warn); + } else if (claudeThinkingType === 'enabled') { + upsert('CLAUDE_THINKING_BUDGET', parseInt(claudeThinkingBudget, 10) || 16000, false).catch(console.warn); + } + } + await changeModel(sessionId, modelObj); onModelSelected?.(modelObj.name); @@ -364,6 +432,57 @@ export const SwitchModelModal = ({ } }; + const claudeThinkingTypeOptions = [ + ...(modelSupportsAdaptive + ? [{ value: 'adaptive', label: 'Adaptive - Claude decides when and how much to think' }] + : []), + { value: 'enabled', label: 'Enabled - Fixed token budget for thinking' }, + { value: 'disabled', label: 'Disabled - No extended thinking' }, + ]; + + const claudeThinkingControls = showClaudeThinking && ( +
+
+ + o.value === claudeThinkingEffort)} + onChange={(newValue: unknown) => { + const option = newValue as { value: string; label: string } | null; + setClaudeThinkingEffort(option?.value || 'high'); + }} + placeholder="Select effort level" + /> +
+ )} + {claudeThinkingType === 'enabled' && ( +
+ + setClaudeThinkingBudget(e.target.value)} + /> +
+ )} +
+ ); + return ( @@ -455,6 +574,8 @@ export const SwitchModelModal = ({ /> )} + + {claudeThinkingControls} ) : ( /* Manual Provider/Model Selection */ @@ -571,6 +692,8 @@ export const SwitchModelModal = ({ /> )} + + {claudeThinkingControls} )}