Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
d2faa1e
Add unified thinking effort control across all providers
jh-block Mar 3, 2026
94dcabe
Remove legacy thinking/reasoning env vars and model-name-suffix effort
jh-block Mar 3, 2026
539abc4
Migrate legacy model name effort suffixes at ModelConfig construction
jh-block Mar 3, 2026
c14a10a
Address PR review comments: fix legacy suffix stripping, merge reques…
jh-block Mar 3, 2026
f857051
Filter thinking_effort from Databricks payload; honor Off for Gemini …
jh-block Mar 3, 2026
842adf3
Avoid overriding suffix-derived thinking effort with default 'off' in…
jh-block Mar 3, 2026
6280d33
Persist displayed default effort on submit; restrict Claude thinking …
jh-block Mar 3, 2026
940e9d3
Normalize effort suffix on deserialized configs; restrict CLI Claude …
jh-block Mar 3, 2026
dc0ee58
Rename request param merge helper and remove Option arg
jh-block Mar 4, 2026
db6cb0c
Normalize legacy effort suffix during ModelConfig deserialization
jh-block Mar 4, 2026
b6f8b2e
Migrate Codex provider to unified thinking effort
jh-block Mar 4, 2026
75146b4
Add unified thinking effort support for chatgpt_codex
jh-block Mar 4, 2026
19def1d
chore: revert cli-providers.md doc change to match origin/main
jh-block Mar 11, 2026
e61cb9e
Address DOsinga's review: drop local ThinkingEffort enum, simplify te…
jh-block Mar 11, 2026
9036c47
Fix unified thinking effort edge cases
jh-block May 15, 2026
ca7d496
Update desktop i18n messages
jh-block May 15, 2026
71bfe3b
Fix ACP model request params merge
jh-block May 15, 2026
932ffa1
Address thinking effort review feedback
jh-block May 15, 2026
fa6b0cc
Clamp OpenAI reasoning efforts by model
jh-block May 15, 2026
02d880e
Preserve Responses API thinking effort
jh-block May 15, 2026
b3f4ee1
Address model config review feedback
jh-block May 15, 2026
5822291
Address thinking effort review feedback
jh-block May 15, 2026
625a9b1
Preserve explicit none thinking effort
jh-block May 15, 2026
dd3cea8
Gate Responses reasoning config by model
jh-block May 15, 2026
50d3548
Address thinking effort review feedback
jh-block May 15, 2026
976fb16
Support OpenRouter thinking effort
jh-block May 15, 2026
9ac3f19
Use Databricks endpoint metadata for thinking
jh-block May 15, 2026
6323f23
Address reasoning effort review feedback
jh-block May 15, 2026
4c12fcb
Address Databricks review feedback
jh-block May 15, 2026
b8e566d
Clear stale model reasoning metadata
jh-block May 15, 2026
5ce81ae
Preserve Gemini 3 thinking level fallback
jh-block May 15, 2026
cbe355c
Preserve thinking effort fallbacks
jh-block May 15, 2026
e2dd83e
Scope legacy thinking budget fallbacks
jh-block May 15, 2026
a413d03
Move reasoning detection to core
jh-block May 15, 2026
de40c37
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
102 changes: 22 additions & 80 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 @@ -738,15 +736,13 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {

let spin = spinner();
spin.start("Attempting to fetch supported models...");
let models_res = {
let temp_model_config =
ModelConfig::new(&provider_meta.default_model)?.with_canonical_limits(provider_name);
let temp_provider = create(provider_name, temp_model_config, Vec::new()).await?;
retry_operation(&RetryConfig::default(), || async {
temp_provider.fetch_recommended_models().await
})
.await
};
let temp_model_config =
ModelConfig::new(&provider_meta.default_model)?.with_canonical_limits(provider_name);
let temp_provider = create(provider_name, temp_model_config, Vec::new()).await?;
let models_res = retry_operation(&RetryConfig::default(), || async {
temp_provider.fetch_recommended_models().await
})
.await;
spin.stop(style("Model fetch complete").green());

// Select a model: on fetch error show styled error and abort; if models available, show list; otherwise free-text input
Expand All @@ -766,78 +762,24 @@ 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)?;
{
let supports_thinking = match temp_provider.fetch_model_info(&model).await {
Ok(model_info) => model_info.reasoning,
Err(_) => goose::model::ModelConfig::new(&model)
.map(|c| c.is_reasoning_model())
.unwrap_or(false),
};

if thinking_type == "adaptive" {
let effort: &str = cliclack::select("Select adaptive thinking effort level:")
.item("low", "Low - Minimal thinking, fastest responses", "")
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")
.item("high", "High - Deep reasoning", "")
.item("max", "Max - No constraints on thinking depth", "")
.initial_value("off")
.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>()?)?;
}
}

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
11 changes: 8 additions & 3 deletions crates/goose-server/src/routes/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub struct UpdateProviderRequest {
model: Option<String>,
session_id: String,
context_limit: Option<usize>,
reasoning: Option<bool>,
request_params: Option<std::collections::HashMap<String, serde_json::Value>>,
}

Expand Down Expand Up @@ -595,16 +596,20 @@ 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);
}
model_config.reasoning = payload.reasoning;

let extensions =
EnabledExtensionsState::for_session(state.session_manager(), &payload.session_id, config)
Expand Down
62 changes: 58 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: 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,9 @@ 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
.unwrap_or_else(|| ModelConfig::new_or_fail(&query.model).is_reasoning_model()),
// 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 +907,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 @@ -3308,11 +3308,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