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
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ The `config` object holds runtime behaviour options.
| `model` | string \| null | provider default | Model ID to use. When absent, the provider's default is used (e.g. `claude-sonnet-4-6` for Anthropic, `gpt-4o` for OpenAI). |
| `max_tokens` | integer \| null | 8192 | Maximum tokens per model response. |
| `provider` | string \| null | `"anthropic"` | Active provider. See the [Providers](#providers) section. |
| `effort` | string \| null | unset | Reasoning effort for new turns: `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"`, `"max"`, or `"ultracode"`. When unset, no thinking-budget or temperature override is applied at all — this is not the same as `"medium"`, which has its own explicit values. |

### Permission mode

Expand Down
34 changes: 34 additions & 0 deletions src-rust/crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,13 @@ pub mod config {
pub theme: Theme,
#[serde(default)]
pub output_style: Option<String>,
/// Reasoning effort level for new turns (see [`crate::effort::EffortLevel`]).
/// `None` means no effort override is applied at all — the query loop
/// sends no thinking-budget or temperature override, so the model and
/// provider's own defaults take effect. This is *not* the same as
/// `EffortLevel::Medium`, which has its own explicit budget/temperature.
#[serde(default)]
pub effort: Option<String>,
pub auto_compact: bool,
pub compact_threshold: f32,
pub verbose: bool,
Expand Down Expand Up @@ -1456,6 +1463,14 @@ pub mod config {
}


/// Resolve the configured reasoning effort level, if any and valid.
/// Returns `None` when unset or unparseable — the caller (the query
/// loop) then applies no thinking-budget/temperature override at all,
/// not `EffortLevel::Medium`'s specific values.
pub fn effective_effort_level(&self) -> Option<crate::effort::EffortLevel> {
self.effort.as_deref().and_then(crate::effort::EffortLevel::from_str)
}

/// Resolve the effective max-tokens.
pub fn effective_max_tokens(&self) -> u32 {
self.max_tokens
Expand Down Expand Up @@ -1939,6 +1954,7 @@ pub mod config {
permission_mode: over.config.permission_mode,
theme: over.config.theme,
output_style: over.config.output_style.or(base.config.output_style),
effort: over.config.effort.or(base.config.effort),
auto_compact: over.config.auto_compact || base.config.auto_compact,
compact_threshold: if over.config.compact_threshold != 0.0 {
over.config.compact_threshold
Expand Down Expand Up @@ -4761,6 +4777,24 @@ mod tests {
assert_eq!(cfg.effective_model(), "claude-haiku-4-5-20251001");
}

#[test]
fn test_config_effective_effort_level_unset_is_none() {
let cfg = crate::config::Config::default();
assert_eq!(cfg.effective_effort_level(), None);
}

#[test]
fn test_config_effective_effort_level_valid() {
let cfg = crate::config::Config { effort: Some("xhigh".to_string()), ..Default::default() };
assert_eq!(cfg.effective_effort_level(), Some(crate::effort::EffortLevel::XHigh));
}

#[test]
fn test_config_effective_effort_level_invalid_falls_back_to_none() {
let cfg = crate::config::Config { effort: Some("not-a-level".to_string()), ..Default::default() };
assert_eq!(cfg.effective_effort_level(), None);
}

#[test]
fn test_config_effective_max_tokens_default() {
let cfg = crate::config::Config::default();
Expand Down
19 changes: 19 additions & 0 deletions src-rust/crates/query/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ impl QueryConfig {
.as_ref()
.map(|p| p.display().to_string()),
managed_agents: cfg.managed_agents.clone(),
effort_level: cfg.effective_effort_level(),
..Default::default()
}
}
Expand All @@ -231,6 +232,7 @@ impl QueryConfig {
.as_ref()
.map(|p| p.display().to_string()),
managed_agents: cfg.managed_agents.clone(),
effort_level: cfg.effective_effort_level(),
..Default::default()
}
}
Expand Down Expand Up @@ -2122,6 +2124,23 @@ mod tests {
assert_eq!(turn_usage.output_tokens, 75);
}

#[test]
fn from_config_carries_effort_level_through() {
let cfg = claurst_core::config::Config {
effort: Some("high".to_string()),
..Default::default()
};
let query_config = QueryConfig::from_config(&cfg);
assert_eq!(query_config.effort_level, Some(claurst_core::effort::EffortLevel::High));
}

#[test]
fn from_config_unset_effort_is_none() {
let cfg = claurst_core::config::Config::default();
let query_config = QueryConfig::from_config(&cfg);
assert_eq!(query_config.effort_level, None);
}

fn make_config(sys: Option<&str>, append: Option<&str>) -> QueryConfig {
QueryConfig {
model: "claude-sonnet-4-6".to_string(),
Expand Down
68 changes: 65 additions & 3 deletions src-rust/crates/tools/src/config_tool.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// ConfigTool: get or set Claurst configuration settings at runtime.
//
// Reads from and persists to ~/.claurst/settings.json.
// Supported settings: model, max_tokens, verbose, permission_mode.
// Supported settings: model, provider, effort, max_tokens, verbose, permission_mode.

use crate::{PermissionLevel, Tool, ToolContext, ToolResult};
use async_trait::async_trait;
Expand All @@ -16,8 +16,23 @@ struct ConfigInput {
value: Option<Value>,
}

// Provider ids this build actually knows how to construct a client for.
// Mirrors the match arms in claurst_api::registry (create_provider_by_id /
// build_provider) and claurst_core::config::Config::effective_model's
// per-provider default-model table — kept as the primary id only (not every
// alias accepted by those matches, e.g. "lm-studio"/"llama-cpp"). Update this
// list alongside those when a new provider is added.
static KNOWN_PROVIDER_IDS: &[&str] = &[
"anthropic", "openai", "google", "groq", "cerebras", "deepseek", "mistral",
"xai", "openrouter", "togetherai", "perplexity", "cohere", "qwen", "deepinfra",
"github-copilot", "ollama", "lmstudio", "llamacpp", "custom-openai", "azure",
"amazon-bedrock", "venice", "minimax", "codex", "free",
];

static SUPPORTED_SETTINGS: &[(&str, &str)] = &[
("model", "LLM model to use (e.g. 'claude-opus-4-6')"),
("provider", "Active provider id — one of the ids this build supports (use setting='provider' with a bad value to see the full list)"),
("effort", "Reasoning effort: none | minimal | low | medium | high | xhigh | max | ultracode"),
("max_tokens", "Maximum output tokens per response"),
("verbose", "Enable verbose logging (true/false)"),
("permission_mode", "Permission mode: default | accept_edits | bypass_permissions | plan"),
Expand All @@ -30,8 +45,8 @@ impl Tool for ConfigTool {

fn description(&self) -> &str {
"Get or set Claurst configuration settings. Omit 'value' to read the current value. \
Supported settings: model, max_tokens, verbose, permission_mode, auto_compact. \
Changes persist to ~/.claurst/settings.json."
Supported settings: model, provider, effort, max_tokens, verbose, permission_mode, \
auto_compact. Changes persist to ~/.claurst/settings.json."
}

fn permission_level(&self) -> PermissionLevel { PermissionLevel::Write }
Expand Down Expand Up @@ -92,6 +107,41 @@ impl Tool for ConfigTool {
}
ToolResult::success(format!("model = \"{}\"", s))
}
"provider" => {
let s = match new_value.as_str() {
Some(s) => s.to_string(),
None => return ToolResult::error("'provider' must be a string".to_string()),
};
if !KNOWN_PROVIDER_IDS.contains(&s.as_str()) {
return ToolResult::error(format!(
"Unknown provider '{}'. Use one of: {}",
s,
KNOWN_PROVIDER_IDS.join(" | ")
));
}
settings.config.provider = Some(s.clone());
if let Err(e) = settings.save().await {
return ToolResult::error(format!("Failed to save settings: {}", e));
}
ToolResult::success(format!("provider = \"{}\"", s))
}
"effort" => {
let s = match new_value.as_str() {
Some(s) => s,
None => return ToolResult::error("'effort' must be a string".to_string()),
};
if claurst_core::effort::EffortLevel::from_str(s).is_none() {
return ToolResult::error(format!(
"Unknown effort level '{}'. Use: none | minimal | low | medium | high | xhigh | max | ultracode",
s
));
}
settings.config.effort = Some(s.to_string());
if let Err(e) = settings.save().await {
return ToolResult::error(format!("Failed to save settings: {}", e));
}
ToolResult::success(format!("effort = \"{}\"", s))
}
"max_tokens" => {
let n = match new_value.as_u64() {
Some(n) => n as u32,
Expand Down Expand Up @@ -163,6 +213,18 @@ impl Tool for ConfigTool {
"model = \"{}\"",
settings.config.effective_model()
)),
"provider" => ToolResult::success(format!(
"provider = \"{}\"",
settings.config.selected_provider_id()
)),
"effort" => match settings.config.effective_effort_level() {
Some(level) => ToolResult::success(format!("effort = \"{}\"", level.as_str())),
None => ToolResult::success(
"effort = unset (no thinking-budget/temperature override applied; \
model/provider defaults are used, not equivalent to any specific level)"
.to_string(),
),
},
"max_tokens" => ToolResult::success(format!(
"max_tokens = {}",
settings.config.effective_max_tokens()
Expand Down
Loading