mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: support Anthropic adaptive thinking (#7356)
Signed-off-by: rabi <ramishra@redhat.com>
This commit is contained in:
@@ -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;
|
||||
@@ -761,6 +762,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...");
|
||||
|
||||
@@ -1028,6 +1028,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);
|
||||
|
||||
fn find_workspace_or_exe_root() -> Option<PathBuf> {
|
||||
let exe = std::env::current_exe().ok()?;
|
||||
|
||||
@@ -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>,
|
||||
@@ -283,6 +283,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))
|
||||
@@ -356,4 +372,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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<Self, Self::Err> {
|
||||
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::<String>("thinking_type", "CLAUDE_THINKING_TYPE")
|
||||
{
|
||||
let tt = s.parse::<ThinkingType>().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,43 @@ pub fn get_usage(data: &Value) -> Result<Usage> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn thinking_effort(model_config: &ModelConfig) -> ThinkingEffort {
|
||||
match model_config.get_config_param::<String>("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::<i32>("budget_tokens", "CLAUDE_THINKING_BUDGET")
|
||||
.unwrap_or(16000)
|
||||
.max(1024);
|
||||
|
||||
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,29 +536,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 {
|
||||
// Anthropic requires budget_tokens >= 1024
|
||||
const DEFAULT_THINKING_BUDGET: i32 = 16000;
|
||||
let raw_budget_tokens: i32 = std::env::var("CLAUDE_THINKING_BUDGET")
|
||||
.ok()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(DEFAULT_THINKING_BUDGET);
|
||||
let budget_tokens: i32 = std::cmp::max(1024, raw_budget_tokens);
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -701,6 +787,7 @@ where
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conversation::message::Message;
|
||||
use crate::model::ModelConfig;
|
||||
use rmcp::object;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -967,6 +1054,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;
|
||||
@@ -1043,4 +1194,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -547,17 +547,8 @@ fn get_thinking_config(model_config: &ModelConfig) -> Option<ThinkingConfig> {
|
||||
}
|
||||
|
||||
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::<String>("thinking_level", "GEMINI3_THINKING_LEVEL")
|
||||
.map(|s| s.to_lowercase())
|
||||
.or_else(|| {
|
||||
crate::config::Config::global()
|
||||
.get_param::<String>("gemini3_thinking_level")
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase())
|
||||
})
|
||||
.unwrap_or_else(|| "low".to_string());
|
||||
|
||||
let thinking_level = match thinking_level_str.as_str() {
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
const [thinkingLevel, setThinkingLevel] = useState<string>('low');
|
||||
const [claudeThinkingType, setClaudeThinkingType] = useState<string>('disabled');
|
||||
const [claudeThinkingEffort, setClaudeThinkingEffort] = useState<string>('high');
|
||||
const [claudeThinkingBudget, setClaudeThinkingBudget] = useState<string>('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<string | null> => {
|
||||
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<string, unknown> = {
|
||||
...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 && (
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
<div>
|
||||
<label className="text-sm text-textSubtle mb-1 block">Extended Thinking</label>
|
||||
<Select
|
||||
options={claudeThinkingTypeOptions}
|
||||
value={claudeThinkingTypeOptions.find((o) => o.value === claudeThinkingType)}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; label: string } | null;
|
||||
setClaudeThinkingType(option?.value || 'disabled');
|
||||
}}
|
||||
placeholder="Select thinking mode"
|
||||
/>
|
||||
</div>
|
||||
{claudeThinkingType === 'adaptive' && (
|
||||
<div>
|
||||
<label className="text-sm text-textSubtle mb-1 block">Thinking Effort</label>
|
||||
<Select
|
||||
options={CLAUDE_THINKING_EFFORT_OPTIONS}
|
||||
value={CLAUDE_THINKING_EFFORT_OPTIONS.find((o) => o.value === claudeThinkingEffort)}
|
||||
onChange={(newValue: unknown) => {
|
||||
const option = newValue as { value: string; label: string } | null;
|
||||
setClaudeThinkingEffort(option?.value || 'high');
|
||||
}}
|
||||
placeholder="Select effort level"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{claudeThinkingType === 'enabled' && (
|
||||
<div>
|
||||
<label className="text-sm text-textSubtle mb-1 block">Thinking Budget (tokens)</label>
|
||||
<Input
|
||||
className="border-2 px-4 py-2"
|
||||
type="number"
|
||||
min="1024"
|
||||
value={claudeThinkingBudget}
|
||||
onChange={(e) => setClaudeThinkingBudget(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
@@ -455,6 +574,8 @@ export const SwitchModelModal = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{claudeThinkingControls}
|
||||
</div>
|
||||
) : (
|
||||
/* Manual Provider/Model Selection */
|
||||
@@ -600,6 +721,8 @@ export const SwitchModelModal = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{claudeThinkingControls}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user