Add unified thinking effort control across all providers (#9242)

Signed-off-by: jh-block <jhugo@block.xyz>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jh-block
2026-05-20 10:42:42 +02:00
committed by GitHub
parent c467e7f998
commit 98a54e9ec6
30 changed files with 2175 additions and 526 deletions
+4 -1
View File
@@ -5,7 +5,7 @@ use goose::config::permission::PermissionLevel;
use goose::config::ExtensionEntry;
use goose::conversation::Conversation;
use goose::download_manager::{DownloadProgress, DownloadStatus};
use goose::model::ModelConfig;
use goose::model::{ModelConfig, ThinkingEffort};
use goose::permission::permission_confirmation::{Permission, PrincipalType};
use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType};
use goose::session::{Session, SessionInsights, SessionType, SystemInfo};
@@ -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,
@@ -573,6 +574,8 @@ derive_utoipa!(IconTheme as IconThemeSchema);
PrincipalType,
ModelInfo,
ModelConfig,
ThinkingEffort,
super::routes::config_management::ProviderModelInfoQuery,
Session,
goose::config::goose_mode::GooseMode,
SessionInsights,
+11 -3
View File
@@ -1,3 +1,4 @@
use crate::routes::config_management::resolve_provider_model_info;
use crate::routes::errors::ErrorResponse;
use crate::routes::recipe_utils::{
apply_recipe_to_agent, build_recipe_with_parameter_values, load_recipe_by_id, validate_recipe,
@@ -595,7 +596,7 @@ async fn update_agent_provider(
}
};
let model_config = ModelConfig::new(&model)
let mut model_config = ModelConfig::new(&model)
.map_err(|e| {
(
StatusCode::BAD_REQUEST,
@@ -603,8 +604,15 @@ async fn update_agent_provider(
)
})?
.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 model_info = resolve_provider_model_info(&payload.provider, &model)
.await
.map_err(|e| (e.status, e.message))?;
model_config.reasoning = Some(model_info.reasoning);
let extensions =
EnabledExtensionsState::for_session(state.session_manager(), &payload.session_id, config)
@@ -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,
@@ -418,7 +418,7 @@ 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")
@@ -426,7 +426,7 @@ pub async fn providers() -> Result<Json<Vec<ProviderDetails>>, ErrorResponse> {
)]
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!(
@@ -444,7 +444,7 @@ 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)),
@@ -452,6 +452,70 @@ pub async fn get_provider_models(
}
}
#[derive(Deserialize, ToSchema)]
pub struct ProviderModelInfoQuery {
pub model: String,
}
pub async fn resolve_provider_model_info(
name: &str,
model: &str,
) -> Result<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(model)?.with_canonical_limits(name);
let provider = goose::providers::create(name, model_config.clone(), Vec::new()).await?;
match provider.fetch_model_info(model).await {
Ok(info) => Ok(info),
Err(error) => {
let mut info = ModelInfo::new(model, model_config.context_limit());
info.reasoning = model_config.is_reasoning_model();
tracing::debug!(
provider = name,
model,
error = %error,
"Falling back to local model metadata"
);
Ok(info)
}
}
}
#[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> {
resolve_provider_model_info(&name, &query.model)
.await
.map(Json)
}
#[derive(Deserialize, utoipa::IntoParams)]
pub struct SlashCommandsQuery {
/// Optional working directory to discover local skills from
@@ -523,6 +587,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>,
@@ -560,6 +625,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,
@@ -926,6 +994,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}",