Skip to content
Open
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
facbb55
Add unified thinking effort control across all providers
jh-block Mar 3, 2026
0692f5a
Remove legacy thinking/reasoning env vars and model-name-suffix effort
jh-block Mar 3, 2026
13b846a
Migrate legacy model name effort suffixes at ModelConfig construction
jh-block Mar 3, 2026
a0f49c0
Address PR review comments: fix legacy suffix stripping, merge reques…
jh-block Mar 3, 2026
c460f49
Filter thinking_effort from Databricks payload; honor Off for Gemini …
jh-block Mar 3, 2026
f9f1312
Avoid overriding suffix-derived thinking effort with default 'off' in…
jh-block Mar 3, 2026
c8b15b2
Persist displayed default effort on submit; restrict Claude thinking …
jh-block Mar 3, 2026
3e022e0
Normalize effort suffix on deserialized configs; restrict CLI Claude …
jh-block Mar 3, 2026
abae039
Rename request param merge helper and remove Option arg
jh-block Mar 4, 2026
8ace93f
Normalize legacy effort suffix during ModelConfig deserialization
jh-block Mar 4, 2026
4de6aed
Migrate Codex provider to unified thinking effort
jh-block Mar 4, 2026
a3ae644
Add unified thinking effort support for chatgpt_codex
jh-block Mar 4, 2026
8c7c1f0
chore: revert cli-providers.md doc change to match origin/main
jh-block Mar 11, 2026
c24ba82
Address DOsinga's review: drop local ThinkingEffort enum, simplify te…
jh-block Mar 11, 2026
e7706d6
Fix unified thinking effort edge cases
jh-block May 15, 2026
58fa143
Update desktop i18n messages
jh-block May 15, 2026
305dd77
Fix ACP model request params merge
jh-block May 15, 2026
4ec5585
Address thinking effort review feedback
jh-block May 15, 2026
dbe809a
Clamp OpenAI reasoning efforts by model
jh-block May 15, 2026
4b6561a
Preserve Responses API thinking effort
jh-block May 15, 2026
a8084ca
Address model config review feedback
jh-block May 15, 2026
8d5da61
Address thinking effort review feedback
jh-block May 15, 2026
de07979
Preserve explicit none thinking effort
jh-block May 15, 2026
db5e545
Gate Responses reasoning config by model
jh-block May 15, 2026
09ce5ce
Address thinking effort review feedback
jh-block May 15, 2026
00fddd2
Support OpenRouter thinking effort
jh-block May 15, 2026
c0b4bce
Use Databricks endpoint metadata for thinking
jh-block May 15, 2026
53de26d
Address reasoning effort review feedback
jh-block May 15, 2026
ab17833
Address Databricks review feedback
jh-block May 15, 2026
ef49673
Clear stale model reasoning metadata
jh-block May 15, 2026
6aae898
Preserve Gemini 3 thinking level fallback
jh-block May 15, 2026
b494213
Preserve thinking effort fallbacks
jh-block May 15, 2026
c8f0cac
Scope legacy thinking budget fallbacks
jh-block May 15, 2026
6c3901c
Move reasoning detection to core
jh-block May 15, 2026
cc2ad22
Apply OpenRouter reasoning fallback
jh-block May 15, 2026
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
89 changes: 17 additions & 72 deletions crates/goose-cli/src/commands/configure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ use goose::model::ModelConfig;
#[cfg(feature = "telemetry")]
use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY};
use goose::providers::base::ConfigKey;
use goose::providers::chatgpt_codex::reasoning_levels_for_model;
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 @@ -766,78 +764,25 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
}
};

if model.to_lowercase().starts_with("gemini-3") {
let thinking_level: &str = cliclack::select("Select thinking level for Gemini 3:")
.item("low", "Low - Better latency, lighter reasoning", "")
.item("high", "High - Deeper reasoning, higher latency", "")
.interact()?;
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", "")
{
let claude_thinking_providers = ["anthropic", "databricks"];
let supports_thinking = model.to_lowercase().starts_with("gemini-3")
|| (model.to_lowercase().contains("claude")
&& claude_thinking_providers.contains(&provider_name.as_str()))
|| goose::model::ModelConfig::new(&model)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Offer unified thinking prompt for OpenRouter Claude models

The new CLI gating excludes Claude models unless the provider is anthropic or databricks, so goose configure skips the unified GOOSE_THINKING_EFFORT prompt for OpenRouter Claude selections even though this commit adds OpenRouter reasoning support via apply_reasoning_config when a model is reasoning-capable. In practice, configuring openrouter with a reasoning Claude model (for example claude-sonnet-4*) leaves users unable to set thinking effort through the CLI path, making the new feature unavailable from the primary setup flow.

Useful? React with 👍 / 👎.

.map(|c| c.is_openai_reasoning_model())
.unwrap_or(false);

if supports_thinking {
let effort: &str = cliclack::select("Select thinking effort:")
.item("off", "Off - No extended thinking", "")
.item("low", "Low - Better latency, lighter reasoning", "")
.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"),
})
.item("high", "High - Deep reasoning", "")
.item("max", "Max - No constraints on thinking depth", "")
.initial_value("off")
.interact()?;
config.set_claude_thinking_budget(budget.parse::<i32>()?)?;
}
}

if provider_name == "chatgpt_codex" {
let valid_levels = reasoning_levels_for_model(&model);
if !valid_levels.is_empty() {
let mut select = cliclack::select("Select reasoning effort level:");
for &level in valid_levels {
let description = match level {
"low" => "Low - Fast responses with lighter reasoning",
"medium" => "Medium - Balances speed and reasoning depth for everyday tasks",
"high" => "High - Greater reasoning depth for complex problems",
"xhigh" => "Extra High - Extra high reasoning depth for complex problems",
_ => "",
};
select = select.item(level, description, "");
}
select = select.initial_value("medium");
let effort: &str = select.interact()?;
config.set_chatgpt_codex_reasoning_effort(effort.to_string())?;
config.set_goose_thinking_effort(effort)?;
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/goose-cli/src/session/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ fn resolve_provider_and_model(
.is_some_and(|mc| mc.model_name == model_name)
{
let mut config = saved_model_config.unwrap();
config.normalize_effort_suffix();
if let Some(temp) = recipe_settings.and_then(|s| s.temperature) {
config = config.with_temperature(Some(temp));
}
Expand Down
2 changes: 2 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ derive_utoipa!(IconTheme as IconThemeSchema);
super::routes::config_management::read_all_config,
super::routes::config_management::providers,
super::routes::config_management::get_provider_models,
super::routes::config_management::get_provider_model_info,
super::routes::config_management::get_slash_commands,
super::routes::config_management::upsert_permissions,
super::routes::config_management::create_custom_provider,
Expand Down Expand Up @@ -573,6 +574,7 @@ derive_utoipa!(IconTheme as IconThemeSchema);
PrincipalType,
ModelInfo,
ModelConfig,
super::routes::config_management::ProviderModelInfoQuery,
Session,
goose::config::goose_mode::GooseMode,
SessionInsights,
Expand Down
9 changes: 6 additions & 3 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,16 +595,19 @@ async fn update_agent_provider(
}
};

let model_config = ModelConfig::new(&model)
let mut model_config = ModelConfig::new(&model)
.map_err(|e| {
(
StatusCode::BAD_REQUEST,
format!("Invalid model config: {}", e),
)
})?
.with_canonical_limits(&payload.provider)
.with_context_limit(payload.context_limit)
.with_request_params(payload.request_params);
.with_context_limit(payload.context_limit);

if let Some(request_params) = payload.request_params {
model_config = model_config.with_merged_request_params(request_params);
}

let extensions =
EnabledExtensionsState::for_session(state.session_manager(), &payload.session_id, config)
Expand Down
60 changes: 56 additions & 4 deletions crates/goose-server/src/routes/config_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use goose::config::ExtensionEntry;
use goose::config::{Config, ConfigError};
use goose::custom_requests::SourceType;
use goose::model::ModelConfig;
use goose::providers::base::{ProviderMetadata, ProviderType};
use goose::providers::base::{ModelInfo, ProviderMetadata, ProviderType};
use goose::providers::canonical::maybe_get_canonical_model;
use goose::providers::catalog::{
get_provider_template, get_providers_by_format, ProviderCatalogEntry, ProviderFormat,
Expand Down Expand Up @@ -366,15 +366,15 @@ pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, ErrorResponse> {
("name" = String, Path, description = "Provider name (e.g., openai)")
),
responses(
(status = 200, description = "Models fetched successfully", body = [String]),
(status = 200, description = "Models fetched successfully", body = [ModelInfo]),
(status = 400, description = "Unknown provider, provider not configured, or authentication error"),
(status = 429, description = "Rate limit exceeded"),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_provider_models(
Path(name): Path<String>,
) -> Result<Json<Vec<String>>, ErrorResponse> {
) -> Result<Json<Vec<ModelInfo>>, ErrorResponse> {
let all = get_providers().await.into_iter().collect::<Vec<_>>();
let Some((metadata, provider_type)) = all.into_iter().find(|(m, _)| m.name == name) else {
return Err(ErrorResponse::bad_request(format!(
Expand All @@ -392,14 +392,60 @@ pub async fn get_provider_models(
let model_config = ModelConfig::new(&metadata.default_model)?.with_canonical_limits(&name);
let provider = goose::providers::create(&name, model_config, Vec::new()).await?;

let models_result = provider.fetch_recommended_models().await;
let models_result = provider.fetch_recommended_model_info().await;

match models_result {
Ok(models) => Ok(Json(models)),
Err(provider_error) => Err(provider_error.into()),
}
}

#[derive(Deserialize, ToSchema)]
pub struct ProviderModelInfoQuery {
pub model: String,
}

#[utoipa::path(
post,
path = "/config/providers/{name}/model-info",
params(
("name" = String, Path, description = "Provider name (e.g., openai)")
),
request_body = ProviderModelInfoQuery,
responses(
(status = 200, description = "Model metadata fetched successfully", body = ModelInfo),
(status = 400, description = "Unknown provider, provider not configured, or authentication error"),
(status = 429, description = "Rate limit exceeded"),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_provider_model_info(
Path(name): Path<String>,
Json(query): Json<ProviderModelInfoQuery>,
) -> Result<Json<ModelInfo>, ErrorResponse> {
let all = get_providers().await.into_iter().collect::<Vec<_>>();
let Some((metadata, provider_type)) = all.into_iter().find(|(m, _)| m.name == name) else {
return Err(ErrorResponse::bad_request(format!(
"Unknown provider: {}",
name
)));
};
if !check_provider_configured(&metadata, provider_type) {
return Err(ErrorResponse::bad_request(format!(
"Provider '{}' is not configured",
name
)));
}

let model_config = ModelConfig::new(&query.model)?.with_canonical_limits(&name);
let provider = goose::providers::create(&name, model_config, Vec::new()).await?;
provider
.fetch_model_info(&query.model)
.await
.map(Json)
.map_err(Into::into)
}

#[derive(Deserialize, utoipa::IntoParams)]
pub struct SlashCommandsQuery {
/// Optional working directory to discover local skills from
Expand Down Expand Up @@ -471,6 +517,7 @@ pub struct ModelInfoData {
pub model: String,
pub context_limit: usize,
pub max_output_tokens: Option<usize>,
pub reasoning: Option<bool>,
pub input_token_cost: Option<f64>,
pub output_token_cost: Option<f64>,
pub cache_read_token_cost: Option<f64>,
Expand Down Expand Up @@ -508,6 +555,7 @@ pub async fn get_canonical_model_info(
model: query.model.clone(),
context_limit: canonical_model.limit.context,
max_output_tokens: canonical_model.limit.output,
reasoning: canonical_model.reasoning,
// Costs are per million tokens - client handles division for display
input_token_cost: canonical_model.cost.input,
output_token_cost: canonical_model.cost.output,
Expand Down Expand Up @@ -857,6 +905,10 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/config/extensions/{name}", delete(remove_extension))
.route("/config/providers", get(providers))
.route("/config/providers/{name}/models", get(get_provider_models))
.route(
"/config/providers/{name}/model-info",
post(get_provider_model_info),
)
.route("/config/provider-catalog", get(get_provider_catalog))
.route(
"/config/provider-catalog/{id}",
Expand Down
9 changes: 6 additions & 3 deletions crates/goose/src/acp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3256,11 +3256,14 @@ impl GooseAcpAgent {
current_model
};
let model = model_name.unwrap_or(&default_model);
let model_config = crate::model::ModelConfig::new(model)
let mut model_config = crate::model::ModelConfig::new(model)
.invalid_params_err_ctx("Invalid model config")?
.with_canonical_limits(&resolved_provider_name)
.with_context_limit(context_limit)
.with_request_params(request_params);
.with_context_limit(context_limit);

if let Some(request_params) = request_params {
model_config = model_config.with_merged_request_params(request_params);
}

let extensions =
EnabledExtensionsState::for_session(&self.session_manager, session_id, &config).await;
Expand Down
45 changes: 40 additions & 5 deletions crates/goose/src/config/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1024,7 +1024,6 @@ config_value!(CLAUDE_CODE_COMMAND, String, "claude");
config_value!(GEMINI_CLI_COMMAND, String, "gemini");
config_value!(CURSOR_AGENT_COMMAND, String, "cursor-agent");
config_value!(CODEX_COMMAND, String, "codex");
config_value!(CODEX_REASONING_EFFORT, String, "high");
config_value!(CODEX_ENABLE_SKILLS, String, "true");
config_value!(CODEX_SKIP_GIT_CHECK, String, "false");
config_value!(CHATGPT_CODEX_REASONING_EFFORT, String, "medium");
Expand All @@ -1038,12 +1037,48 @@ config_value!(GOOSE_PROMPT_EDITOR_ALWAYS, Option<bool>);
config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize);
config_value!(GOOSE_DISABLE_SESSION_NAMING, bool);
config_value!(GOOSE_DISABLE_TOOL_CALL_SUMMARY, 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);
config_value!(GOOSE_THINKING_EFFORT, String);
config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64);

fn find_workspace_or_exe_root() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let exe_dir = exe.parent()?.to_path_buf();

let mut path = exe;
while let Some(parent) = path.parent() {
let cargo_toml = parent.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
if content.contains("[workspace]") {
return Some(parent.to_path_buf());
}
}
}
path = parent.to_path_buf();
}

Some(exe_dir)
}

pub fn load_init_config_from_workspace() -> Result<Mapping, ConfigError> {
let root = find_workspace_or_exe_root().ok_or_else(|| {
ConfigError::FileError(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not determine executable path",
))
})?;

let init_config_path = root.join("init-config.yaml");
if !init_config_path.exists() {
return Err(ConfigError::NotFound(
"init-config.yaml not found".to_string(),
));
}

let init_content = std::fs::read_to_string(&init_config_path)?;
parse_yaml_content(&init_content)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading