From 80eaf8fa4436557aed82cee6871f23af17449ea1 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Thu, 18 Jun 2026 12:33:19 -0400 Subject: [PATCH] move global config access out of ModelConfig, and move ModelConfig into goose_providers (#9769) --- Cargo.lock | 1 + crates/goose-cli/src/commands/configure.rs | 54 +- crates/goose-cli/src/commands/info.rs | 5 +- crates/goose-cli/src/commands/term.rs | 4 +- .../src/scenario_tests/scenario_runner.rs | 3 +- crates/goose-cli/src/session/builder.rs | 38 +- crates/goose-cli/src/session/mod.rs | 37 +- crates/goose-providers/Cargo.toml | 1 + crates/goose-providers/src/formats/openai.rs | 141 ++--- crates/goose-providers/src/lib.rs | 1 + .../{goose => goose-providers}/src/model.rs | 536 ++---------------- crates/goose-server/src/openapi.rs | 2 +- crates/goose-server/src/routes/agent.rs | 23 +- .../src/routes/config_management.rs | 13 +- crates/goose-server/src/routes/errors.rs | 2 +- crates/goose/src/acp/provider.rs | 4 +- crates/goose/src/acp/response_builder.rs | 5 +- crates/goose/src/acp/server.rs | 63 +- crates/goose/src/acp/server/providers.rs | 14 +- crates/goose/src/agents/agent.rs | 33 +- .../src/agents/platform_extensions/summon.rs | 19 +- crates/goose/src/agents/reply_parts.rs | 7 +- crates/goose/src/config/base.rs | 202 ++++++- crates/goose/src/context_mgmt/mod.rs | 2 +- crates/goose/src/doctor.rs | 7 +- crates/goose/src/execution/manager.rs | 2 +- crates/goose/src/gateway/handler.rs | 17 +- crates/goose/src/lib.rs | 2 +- crates/goose/src/model_config.rs | 150 +++++ crates/goose/src/providers/amp_acp.rs | 2 +- crates/goose/src/providers/anthropic.rs | 10 +- crates/goose/src/providers/avian.rs | 2 +- crates/goose/src/providers/azure.rs | 2 +- crates/goose/src/providers/base.rs | 2 +- crates/goose/src/providers/bedrock.rs | 2 +- crates/goose/src/providers/chatgpt_codex.rs | 2 +- crates/goose/src/providers/claude_acp.rs | 2 +- crates/goose/src/providers/claude_code.rs | 2 +- crates/goose/src/providers/codex.rs | 2 +- crates/goose/src/providers/codex_acp.rs | 2 +- crates/goose/src/providers/copilot_acp.rs | 2 +- crates/goose/src/providers/cursor_agent.rs | 2 +- crates/goose/src/providers/databricks.rs | 9 +- crates/goose/src/providers/databricks_v2.rs | 14 +- .../goose/src/providers/formats/anthropic.rs | 24 +- crates/goose/src/providers/formats/bedrock.rs | 2 +- .../goose/src/providers/formats/databricks.rs | 2 +- .../src/providers/formats/gcpvertexai.rs | 2 +- crates/goose/src/providers/formats/google.rs | 12 +- .../src/providers/formats/openai_responses.rs | 4 +- .../goose/src/providers/formats/openrouter.rs | 2 +- .../goose/src/providers/formats/snowflake.rs | 6 +- crates/goose/src/providers/gcpvertexai.rs | 2 +- crates/goose/src/providers/gemini_cli.rs | 2 +- crates/goose/src/providers/gemini_oauth.rs | 8 +- crates/goose/src/providers/githubcopilot.rs | 20 +- crates/goose/src/providers/google.rs | 8 +- crates/goose/src/providers/huggingface.rs | 4 +- crates/goose/src/providers/init.rs | 4 +- crates/goose/src/providers/kimicode.rs | 8 +- crates/goose/src/providers/litellm.rs | 11 +- crates/goose/src/providers/local_inference.rs | 11 +- crates/goose/src/providers/nanogpt.rs | 12 +- crates/goose/src/providers/ollama.rs | 37 +- crates/goose/src/providers/openai.rs | 20 +- .../goose/src/providers/openai_compatible.rs | 13 +- crates/goose/src/providers/openrouter.rs | 18 +- crates/goose/src/providers/pi_acp.rs | 2 +- .../goose/src/providers/provider_registry.rs | 19 +- crates/goose/src/providers/provider_test.rs | 5 +- crates/goose/src/providers/sagemaker_tgi.rs | 2 +- crates/goose/src/providers/snowflake.rs | 2 +- crates/goose/src/providers/testprovider.rs | 2 +- crates/goose/src/providers/tetrate.rs | 12 +- crates/goose/src/providers/toolshim.rs | 18 +- crates/goose/src/providers/xai.rs | 2 +- crates/goose/src/providers/xai_oauth.rs | 2 +- crates/goose/src/scheduler.rs | 2 +- crates/goose/src/session/session_manager.rs | 2 +- .../goose/tests/acp_custom_requests_test.rs | 2 +- crates/goose/tests/acp_fixtures/provider.rs | 2 +- .../acp_secret_cache_invalidation_test.rs | 2 +- crates/goose/tests/agent.rs | 10 +- crates/goose/tests/compaction.rs | 2 +- .../tests/local_inference_integration.rs | 8 +- crates/goose/tests/local_inference_perf.rs | 4 +- crates/goose/tests/mcp_integration_test.rs | 2 +- crates/goose/tests/providers.rs | 11 +- .../tests/session_id_propagation_test.rs | 2 +- crates/goose/tests/tetrate_streaming.rs | 2 +- .../docs/getting-started/providers.md | 2 +- .../docs/guides/environment-variables.md | 74 --- 92 files changed, 843 insertions(+), 1027 deletions(-) rename crates/{goose => goose-providers}/src/model.rs (60%) create mode 100644 crates/goose/src/model_config.rs diff --git a/Cargo.lock b/Cargo.lock index b5af4a3fe..18df6af4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4986,6 +4986,7 @@ dependencies = [ "async-stream", "base64 0.22.1", "chrono", + "env-lock", "futures", "once_cell", "regex", diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index b7cce307f..bdcf1ac57 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -19,13 +19,13 @@ use goose::config::{ configure_tetrate, Config, ConfigError, ExperimentManager, ExtensionEntry, GooseMode, PermissionManager, }; -use goose::model::ModelConfig; #[cfg(feature = "telemetry")] use goose::posthog::{get_telemetry_choice, TELEMETRY_ENABLED_KEY}; use goose::providers::base::ConfigKey; use goose::providers::provider_test::test_provider_configuration; use goose::providers::{create, providers, retry_operation, RetryConfig}; use goose::session::SessionType; +use goose_providers::thinking::ThinkingEffort; use serde_json::Value; use std::collections::HashMap; use std::io::IsTerminal; @@ -339,7 +339,7 @@ async fn handle_oauth_configuration(provider_name: &str, key_name: &str) -> anyh )); // Create a temporary provider instance to handle OAuth - let temp_model = ModelConfig::new("temp")?.with_canonical_limits(provider_name); + let temp_model = goose::model_config::model_config_from_user_config(provider_name, "temp")?; match create(provider_name, temp_model, Vec::new()).await { Ok(provider) => match provider.configure_oauth().await { Ok(_) => { @@ -736,8 +736,10 @@ pub async fn configure_provider_dialog() -> anyhow::Result { let spin = spinner(); spin.start("Attempting to fetch supported models..."); - let temp_model_config = - ModelConfig::new(&provider_meta.default_model)?.with_canonical_limits(provider_name); + let temp_model_config = goose::model_config::model_config_from_user_config( + provider_name, + &provider_meta.default_model, + )?; 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 @@ -765,20 +767,22 @@ pub async fn configure_provider_dialog() -> anyhow::Result { { let supports_thinking = match temp_provider.fetch_model_info(&model).await { Ok(model_info) => model_info.reasoning, - Err(_) => goose::model::ModelConfig::new(&model) + Err(_) => goose_providers::model::ModelConfig::new(&model) .map(|c| c.is_reasoning_model()) .unwrap_or(false), }; if supports_thinking { - let effort: &str = cliclack::select("Select thinking effort:") + let effort: ThinkingEffort = 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", "") .item("max", "Max - No constraints on thinking depth", "") .initial_value("off") - .interact()?; + .interact()? + .parse() + .map_err(|_| anyhow::anyhow!("invalid thinking effort"))?; config.set_goose_thinking_effort(effort)?; } } @@ -1552,7 +1556,7 @@ pub async fn configure_tool_permissions_dialog() -> anyhow::Result<()> { let model: String = config .get_goose_model() .expect("No model configured. Please set model first"); - let model_config = ModelConfig::new(&model)?.with_canonical_limits(&provider_name); + let model_config = goose::model_config::model_config_from_user_config(&provider_name, &model)?; let agent = Agent::new(); @@ -1772,14 +1776,15 @@ pub async fn handle_openrouter_auth() -> anyhow::Result<()> { // Test configuration - get the model that was configured println!("\nTesting configuration..."); let configured_model: String = config.get_goose_model()?; - let model_config = match goose::model::ModelConfig::new(&configured_model) { - Ok(config) => config.with_canonical_limits("openrouter"), - Err(e) => { - eprintln!("⚠️ Invalid model configuration: {}", e); - eprintln!("Your settings have been saved. Please check your model configuration."); - return Ok(()); - } - }; + let model_config = + match goose::model_config::model_config_from_user_config("openrouter", &configured_model) { + Ok(config) => config, + Err(e) => { + eprintln!("⚠️ Invalid model configuration: {}", e); + eprintln!("Your settings have been saved. Please check your model configuration."); + return Ok(()); + } + }; match create("openrouter", model_config, Vec::new()).await { Ok(provider) => { @@ -1853,14 +1858,15 @@ pub async fn handle_tetrate_auth() -> anyhow::Result<()> { // Test configuration println!("\nTesting configuration..."); let configured_model: String = config.get_goose_model()?; - let model_config = match goose::model::ModelConfig::new(&configured_model) { - Ok(config) => config.with_canonical_limits("tetrate"), - Err(e) => { - eprintln!("⚠️ Invalid model configuration: {}", e); - eprintln!("Your settings have been saved. Please check your model configuration."); - return Ok(()); - } - }; + let model_config = + match goose::model_config::model_config_from_user_config("tetrate", &configured_model) { + Ok(config) => config, + Err(e) => { + eprintln!("⚠️ Invalid model configuration: {}", e); + eprintln!("Your settings have been saved. Please check your model configuration."); + return Ok(()); + } + }; match create("tetrate", model_config, Vec::new()).await { Ok(provider) => { diff --git a/crates/goose-cli/src/commands/info.rs b/crates/goose-cli/src/commands/info.rs index 821e7acba..447d1a8ed 100644 --- a/crates/goose-cli/src/commands/info.rs +++ b/crates/goose-cli/src/commands/info.rs @@ -73,9 +73,8 @@ async fn check_provider( } }; - let model_config = goose::model::ModelConfig::new(&model) - .map_err(|e| ProviderCheckError::InvalidModel(e.to_string()))? - .with_canonical_limits(&provider); + let model_config = goose::model_config::model_config_from_user_config(&provider, &model) + .map_err(|e| ProviderCheckError::InvalidModel(e.to_string()))?; let provider_client = goose::providers::create(&provider, model_config, Vec::new()) .await diff --git a/crates/goose-cli/src/commands/term.rs b/crates/goose-cli/src/commands/term.rs index 8596eeb78..17ecdfcb6 100644 --- a/crates/goose-cli/src/commands/term.rs +++ b/crates/goose-cli/src/commands/term.rs @@ -345,9 +345,7 @@ pub async fn handle_term_info() -> Result<()> { .ok() .and_then(|model_name| { config.get_goose_provider().ok().and_then(|provider_name| { - goose::model::ModelConfig::new(&model_name) - .ok() - .map(|c| c.with_canonical_limits(&provider_name)) + goose::model_config::model_config_from_user_config(&provider_name, &model_name).ok() }) }) .map(|mc| mc.context_limit()) diff --git a/crates/goose-cli/src/scenario_tests/scenario_runner.rs b/crates/goose-cli/src/scenario_tests/scenario_runner.rs index 626c883b1..9619f3bdc 100644 --- a/crates/goose-cli/src/scenario_tests/scenario_runner.rs +++ b/crates/goose-cli/src/scenario_tests/scenario_runner.rs @@ -9,7 +9,6 @@ use anyhow::Result; use goose::agents::{Agent, AgentConfig, GoosePlatform}; use goose::config::permission::PermissionManager; use goose::config::GooseMode; -use goose::model::ModelConfig; use goose::providers::{create, testprovider::TestProvider}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; @@ -188,7 +187,7 @@ where let inner_provider = create( &factory_name, - ModelConfig::new(config.model_name)?.with_canonical_limits(&factory_name), + goose::model_config::model_config_from_user_config(&factory_name, config.model_name)?, Vec::new(), ) .await?; diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 6945e6e09..5639a01fc 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -6,6 +6,7 @@ use console::style; use goose::agents::{Agent, Container, ExtensionError}; use goose::config::resolve_extensions_for_new_session; use goose::config::{Config, ExtensionConfig, GooseMode}; +use goose::model_config::model_config_from_user_config; use goose::providers::create; use goose::recipe::Recipe; use goose::session::session_manager::SessionType; @@ -229,14 +230,14 @@ async fn load_extensions( struct ResolvedProviderConfig { provider_name: String, model_name: String, - model_config: goose::model::ModelConfig, + model_config: goose_providers::model::ModelConfig, } fn resolve_provider_and_model( session_config: &SessionBuilderConfig, config: &Config, saved_provider: Option, - saved_model_config: Option, + saved_model_config: Option, ) -> ResolvedProviderConfig { let recipe_settings = session_config .recipe @@ -277,14 +278,16 @@ fn resolve_provider_and_model( } config } else { - let temperature = recipe_settings.and_then(|s| s.temperature); - goose::model::ModelConfig::new(&model_name) - .unwrap_or_else(|e| { - output::render_error(&format!("Failed to create model configuration: {}", e)); - process::exit(1); - }) - .with_canonical_limits(&provider_name) - .with_temperature(temperature) + let mut config = + goose::model_config::model_config_from_user_config(&provider_name, &model_name) + .unwrap_or_else(|e| { + output::render_error(&format!("Failed to create model configuration: {}", e)); + process::exit(1); + }); + if let Some(temp) = recipe_settings.and_then(|s| s.temperature) { + config = config.with_temperature(Some(temp)); + } + config }; ResolvedProviderConfig { @@ -575,12 +578,15 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { )) .yellow() ); - let fallback_model_config = goose::model::ModelConfig::new(&fallback_model) - .unwrap_or_else(|e| { - output::render_error(&format!("Failed to create model configuration: {}", e)); - process::exit(1); - }) - .with_canonical_limits(&fallback_provider); + let fallback_model_config = + model_config_from_user_config(fallback_provider.as_str(), &fallback_model) + .unwrap_or_else(|e| { + output::render_error(&format!( + "Failed to create model configuration: {}", + e + )); + process::exit(1); + }); match create( &fallback_provider, fallback_model_config, diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 0a4f1ceab..b3027901b 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -13,6 +13,7 @@ use crate::session::task_execution_display::{ format_task_execution_notification, TASK_EXECUTION_NOTIFICATION_TYPE, }; use goose::conversation::Conversation; +use std::env; use std::io::Write; use std::str::FromStr; use tokio::signal::ctrl_c; @@ -57,6 +58,8 @@ use tokio; use tokio_util::sync::CancellationToken; use tracing::warn; +const GOOSE_PLANNER_CONTEXT_LIMIT: &str = "GOOSE_PLANNER_CONTEXT_LIMIT"; + #[derive(Serialize, Deserialize, Debug)] struct JsonOutput { messages: Vec, @@ -842,8 +845,11 @@ impl CliSession { let new_model_config = build_switched_model_config(¤t_provider_name, model_name, ¤t_model_config)?; + let configured_effort = Config::global().get_goose_thinking_effort(); + let new_effort = new_model_config.thinking_effort().or(configured_effort); + let current_effort = current_model_config.thinking_effort().or(configured_effort); if new_model_config.model_name == current_model_config.model_name - && new_model_config.thinking_effort() == current_model_config.thinking_effort() + && new_effort == current_effort { output::goose_mode_message(&format!( "Session already using model '{}' for provider '{}'", @@ -2200,7 +2206,6 @@ fn handle_agent_error(e: &anyhow::Error, is_stream_json_mode: bool) { } async fn get_reasoner() -> Result, anyhow::Error> { - use goose::model::ModelConfig; use goose::providers::create; let config = Config::global(); @@ -2225,8 +2230,19 @@ async fn get_reasoner() -> Result, anyhow::Error> { .expect("No model configured. Run 'goose configure' first") }; + let planner_context_limit = match env::var(GOOSE_PLANNER_CONTEXT_LIMIT) + .ok() + .map(|v| v.parse::()) + { + Some(Ok(n)) if n >= 4096 => Some(n), + Some(Ok(_)) => anyhow::bail!("{} must be at least 4096", GOOSE_PLANNER_CONTEXT_LIMIT), + Some(Err(e)) => anyhow::bail!("{}: {}", GOOSE_PLANNER_CONTEXT_LIMIT, e), + None => None, + }; + let model_config = - ModelConfig::new_with_context_env(model, &provider, Some("GOOSE_PLANNER_CONTEXT_LIMIT"))?; + goose::model_config::model_config_from_user_config(&provider, model.as_str())? + .with_context_limit(planner_context_limit); let extensions = goose::config::extensions::get_enabled_extensions_with_config(config); let reasoner = create(&provider, model_config, extensions).await?; @@ -2249,12 +2265,11 @@ fn format_elapsed_time(duration: std::time::Duration) -> String { fn build_switched_model_config( provider_name: &str, model_name: &str, - current_model_config: &goose::model::ModelConfig, -) -> Result { - goose::model::ModelConfig::new(model_name) + current_model_config: &goose_providers::model::ModelConfig, +) -> Result { + goose::model_config::model_config_from_user_config(provider_name, model_name) .map(|config| { config - .with_canonical_limits(provider_name) .with_temperature(current_model_config.temperature) .with_toolshim(current_model_config.toolshim) .with_toolshim_model(current_model_config.toolshim_model.clone()) @@ -2404,7 +2419,7 @@ mod tests { ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), ]); - let current_model_config = goose::model::ModelConfig { + let current_model_config = goose_providers::model::ModelConfig { model_name: "gpt-4o".to_string(), context_limit: Some(128_000), temperature: Some(0.25), @@ -2421,7 +2436,7 @@ mod tests { let switched = build_switched_model_config("openai", "gpt-5.4", ¤t_model_config).unwrap(); - let expected = goose::model::ModelConfig::new_or_fail("gpt-5.4") + let expected = goose_providers::model::ModelConfig::new_or_fail("gpt-5.4") .with_canonical_limits("openai") .with_temperature(Some(0.25)) .with_toolshim(true) @@ -2448,8 +2463,8 @@ mod tests { ("GOOSE_THINKING_EFFORT", None::<&str>), ]); - let current = - goose::model::ModelConfig::new_or_fail("gpt-5.4-high").with_canonical_limits("openai"); + let current = goose_providers::model::ModelConfig::new_or_fail("gpt-5.4-high") + .with_canonical_limits("openai"); assert_eq!(current.model_name, "gpt-5.4"); assert_eq!( current.thinking_effort(), diff --git a/crates/goose-providers/Cargo.toml b/crates/goose-providers/Cargo.toml index 267d2b91c..54c92d34d 100644 --- a/crates/goose-providers/Cargo.toml +++ b/crates/goose-providers/Cargo.toml @@ -34,3 +34,4 @@ test-case = { workspace = true } tempfile = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } +env-lock = { workspace = true } diff --git a/crates/goose-providers/src/formats/openai.rs b/crates/goose-providers/src/formats/openai.rs index 6eefb31da..836a01212 100644 --- a/crates/goose-providers/src/formats/openai.rs +++ b/crates/goose-providers/src/formats/openai.rs @@ -4,6 +4,7 @@ use crate::errors::ProviderError; use crate::images::{convert_image, detect_image_path, load_image_file, ImageFormat}; use crate::json::safely_parse_json; use crate::mcp_utils::extract_text_from_resource; +use crate::model::ModelConfig; use crate::thinking::{ split_think_blocks, ThinkFilter, ThinkingEffort, GEMINI_THOUGHT_SIGNATURE_KEY, }; @@ -1226,7 +1227,7 @@ where } pub fn create_request( - model_config: ModelConfigParams, + model_config: &ModelConfig, system: &str, messages: &[Message], tools: &[Tool], @@ -1246,16 +1247,8 @@ pub fn create_request( ) } -pub struct ModelConfigParams<'a> { - pub model_name: &'a str, - pub thinking_effort: Option, - pub temperature: Option, - pub max_tokens: Option, - pub request_params: Option<&'a HashMap>, -} - pub fn create_request_with_options( - model_config: ModelConfigParams, + model_config: &ModelConfig, system: &str, messages: &[Message], tools: &[Tool], @@ -1269,11 +1262,11 @@ pub fn create_request_with_options( )); } - let (model_name, legacy_reasoning_effort) = extract_reasoning_effort(model_config.model_name); + let (model_name, legacy_reasoning_effort) = extract_reasoning_effort(&model_config.model_name); let is_reasoning_model = is_openai_responses_model(&model_name); let reasoning_effort = if is_reasoning_model { model_config - .thinking_effort + .thinking_effort() .map_or(legacy_reasoning_effort, |effort| { openai_reasoning_effort_for_thinking(&model_name, effort) }) @@ -1335,7 +1328,7 @@ pub fn create_request_with_options( payload["stream_options"] = json!({"include_usage": true}); } - if let Some(params) = model_config.request_params { + if let Some(params) = &model_config.request_params { if let Some(obj) = payload.as_object_mut() { for (key, value) in params { if key != "thinking_effort" && !is_reserved_request_param_key(key) { @@ -1458,6 +1451,13 @@ mod tests { use tokio::pin; use tokio_stream::{self, StreamExt}; + fn test_model_config(model_name: &str) -> ModelConfig { + ModelConfig { + model_name: model_name.to_string(), + ..Default::default() + } + } + #[test] fn test_validate_tool_schemas() { // Test case 1: Empty parameters object @@ -2141,15 +2141,9 @@ mod tests { #[test] fn test_create_request_gpt_4o() -> anyhow::Result<()> { // Test default medium reasoning effort for O3 model - let model_config = ModelConfigParams { - model_name: "gpt-4o", - thinking_effort: None, - temperature: None, - max_tokens: Some(1024), - request_params: None, - }; + let model_config = test_model_config("gpt-4o").with_max_tokens(Some(1024)); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -2180,15 +2174,9 @@ mod tests { // Unknown models on OpenAI-compatible local providers (llama_swap, // lmstudio) have no canonical record and no GOOSE_MAX_TOKENS, so the // request must not pin the legacy 4096 default. See issue #9007. - let model_config = ModelConfigParams { - model_name: "some-unknown-local-model", - thinking_effort: None, - temperature: None, - max_tokens: None, - request_params: None, - }; + let model_config = test_model_config("some-unknown-local-model"); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -2228,15 +2216,18 @@ mod tests { ("temperature".to_string(), json!(2.0)), ("provider_custom".to_string(), json!("allowed")), ]); - let model_config = ModelConfigParams { - model_name: "glm-4.7", - thinking_effort: None, - temperature: None, - max_tokens: Some(4096), - request_params: Some(¶ms), - }; + let model_config = test_model_config("glm-4.7") + .with_max_tokens(Some(4096)) + .with_merged_request_params(params); - let request = create_request(model_config, "system", &[], &[], &ImageFormat::OpenAi, true)?; + let request = create_request( + &model_config, + "system", + &[], + &[], + &ImageFormat::OpenAi, + true, + )?; assert_eq!( request["thinking"], @@ -2258,15 +2249,9 @@ mod tests { #[test] fn test_create_request_o1_default() -> anyhow::Result<()> { - let model_config = ModelConfigParams { - model_name: "o1", - thinking_effort: None, - temperature: None, - max_tokens: Some(1024), - request_params: None, - }; + let model_config = test_model_config("o1").with_max_tokens(Some(1024)); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -2298,17 +2283,11 @@ mod tests { #[test] fn test_create_request_o1_medium_effort() -> anyhow::Result<()> { - let mut params = std::collections::HashMap::new(); - params.insert("thinking_effort".to_string(), json!("medium")); - let model_config = ModelConfigParams { - model_name: "o1", - thinking_effort: Some(ThinkingEffort::Medium), - temperature: None, - max_tokens: Some(1024), - request_params: Some(¶ms), - }; + let model_config = test_model_config("o1") + .with_max_tokens(Some(1024)) + .with_thinking_effort(ThinkingEffort::Medium); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -2325,17 +2304,11 @@ mod tests { #[test] fn test_create_request_o3_off_effort_preserves_none() -> anyhow::Result<()> { - let mut params = std::collections::HashMap::new(); - params.insert("thinking_effort".to_string(), json!("off")); - let model_config = ModelConfigParams { - model_name: "o3", - thinking_effort: Some(ThinkingEffort::Off), - temperature: None, - max_tokens: Some(1024), - request_params: Some(¶ms), - }; + let model_config = test_model_config("o3") + .with_max_tokens(Some(1024)) + .with_thinking_effort(ThinkingEffort::Off); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -2352,17 +2325,11 @@ mod tests { #[test] fn test_create_request_gpt5_pro_max_effort_uses_supported_level() -> anyhow::Result<()> { - let mut params = std::collections::HashMap::new(); - params.insert("thinking_effort".to_string(), json!("max")); - let model_config = ModelConfigParams { - model_name: "gpt-5.2-pro-2025-12-11", - thinking_effort: Some(ThinkingEffort::Max), - temperature: None, - max_tokens: Some(1024), - request_params: Some(¶ms), - }; + let model_config = test_model_config("gpt-5.2-pro-2025-12-11") + .with_max_tokens(Some(1024)) + .with_thinking_effort(ThinkingEffort::Max); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -2379,17 +2346,11 @@ mod tests { #[test] fn test_create_request_o3_custom_reasoning_effort() -> anyhow::Result<()> { - let mut params = std::collections::HashMap::new(); - params.insert("thinking_effort".to_string(), json!("high")); - let model_config = ModelConfigParams { - model_name: "o3-mini", - thinking_effort: Some(ThinkingEffort::High), - temperature: None, - max_tokens: Some(1024), - request_params: Some(¶ms), - }; + let model_config = test_model_config("o3-mini") + .with_max_tokens(Some(1024)) + .with_thinking_effort(ThinkingEffort::High); let request = create_request( - model_config, + &model_config, "system", &[], &[], @@ -3006,13 +2967,7 @@ data: [DONE]"#; #[test] fn test_create_request_preserves_reasoning_content_for_legacy_compat() -> anyhow::Result<()> { - let model_config = ModelConfigParams { - model_name: "deepseek-reasoner", - thinking_effort: None, - temperature: None, - max_tokens: Some(1024), - request_params: None, - }; + let model_config = test_model_config("deepseek-reasoner").with_max_tokens(Some(1024)); let message = Message::assistant() .with_content(MessageContent::thinking("preserve this", "")) .with_tool_request( @@ -3022,7 +2977,7 @@ data: [DONE]"#; ); let request = create_request( - model_config, + &model_config, "system", &[message], &[], diff --git a/crates/goose-providers/src/lib.rs b/crates/goose-providers/src/lib.rs index 17a6d4e23..ae7f7e7aa 100644 --- a/crates/goose-providers/src/lib.rs +++ b/crates/goose-providers/src/lib.rs @@ -6,5 +6,6 @@ pub mod formats; pub mod images; pub mod json; pub(crate) mod mcp_utils; +pub mod model; pub mod thinking; pub mod utils; diff --git a/crates/goose/src/model.rs b/crates/goose-providers/src/model.rs similarity index 60% rename from crates/goose/src/model.rs rename to crates/goose-providers/src/model.rs index f7c5d97da..22159af5d 100644 --- a/crates/goose/src/model.rs +++ b/crates/goose-providers/src/model.rs @@ -1,6 +1,5 @@ -use goose_providers::formats::openai::{extract_reasoning_effort, is_openai_responses_model}; -use goose_providers::thinking::ThinkingEffort; -use once_cell::sync::Lazy; +use crate::formats::openai::{extract_reasoning_effort, is_openai_responses_model}; +use crate::thinking::ThinkingEffort; use serde::de::Deserializer; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -10,33 +9,6 @@ use utoipa::ToSchema; pub const DEFAULT_CONTEXT_LIMIT: usize = 128_000; -#[derive(Debug, Clone, Deserialize)] -struct PredefinedModel { - name: String, - #[serde(default)] - context_limit: Option, - #[serde(default)] - request_params: Option>, -} - -fn get_predefined_models() -> Vec { - static PREDEFINED_MODELS: Lazy> = - Lazy::new(|| match std::env::var("GOOSE_PREDEFINED_MODELS") { - Ok(json_str) => serde_json::from_str(&json_str).unwrap_or_else(|e| { - tracing::warn!("Failed to parse GOOSE_PREDEFINED_MODELS: {}", e); - Vec::new() - }), - Err(_) => Vec::new(), - }); - PREDEFINED_MODELS.clone() -} - -fn find_predefined_model(model_name: &str) -> Option { - get_predefined_models() - .into_iter() - .find(|m| m.name == model_name) -} - #[derive(Error, Debug)] pub enum ConfigError { #[error("Environment variable '{0}' not found")] @@ -103,82 +75,16 @@ impl<'de> Deserialize<'de> for ModelConfig { } impl ModelConfig { - pub fn new(model_name: &str) -> Result { - Self::new_base(model_name.to_string(), None) - } - - pub fn new_with_context_env( - model_name: String, - provider_name: &str, - context_env_var: Option<&str>, - ) -> Result { - let config = Self::new_base(model_name, context_env_var)?; - Ok(config.with_canonical_limits(provider_name)) - } - - fn new_base(model_name: String, context_env_var: Option<&str>) -> Result { - // Check a provider-specific env var first (e.g. DATABRICKS_CONTEXT_LIMIT), - // then fall back to GOOSE_CONTEXT_LIMIT. Using Config::global().get_param() - // reads from both environment variables and config.yaml, so users can set - // `GOOSE_CONTEXT_LIMIT: 1000000` in config.yaml instead of exporting an - // env var. See #7839. - let context_limit = if let Some(env_var) = context_env_var { - if let Ok(val) = std::env::var(env_var) { - Some(Self::validate_context_limit(&val, env_var)?) - } else { - None - } - } else { - match crate::config::Config::global().get_param::("GOOSE_CONTEXT_LIMIT") { - Ok(limit) => { - if limit == 0 { - return Err(ConfigError::InvalidRange( - "GOOSE_CONTEXT_LIMIT".to_string(), - "must be greater than 0".to_string(), - )); - } - Some(limit) - } - Err(crate::config::ConfigError::NotFound(_)) => None, - // Quoted YAML values (e.g. `GOOSE_CONTEXT_LIMIT: '200000'`) and - // environment variables deserialize as strings rather than - // integers; fall back to parsing the string form. - Err(_) => { - match crate::config::Config::global().get_param::("GOOSE_CONTEXT_LIMIT") - { - Ok(val) => Some(Self::validate_context_limit(&val, "GOOSE_CONTEXT_LIMIT")?), - Err(crate::config::ConfigError::NotFound(_)) => None, - Err(e) => { - return Err(ConfigError::InvalidValue( - "GOOSE_CONTEXT_LIMIT".to_string(), - String::new(), - e.to_string(), - )) - } - } - } - } - }; - - let max_tokens = Self::parse_max_tokens()?; - let temperature = Self::parse_temperature()?; - let toolshim = Self::parse_toolshim()?; - let toolshim_model = Self::parse_toolshim_model()?; - - // Pick up predefined model settings before legacy suffix normalization. - let predefined = find_predefined_model(&model_name); - let predefined_context_limit = predefined.as_ref().and_then(|pm| pm.context_limit); - let request_params = predefined.and_then(|pm| pm.request_params); - + pub fn new(model_name: impl AsRef) -> Result { let mut config = Self { - model_name, - context_limit: context_limit.or(predefined_context_limit), - temperature, - max_tokens, - toolshim, - toolshim_model, + model_name: model_name.as_ref().to_string(), + context_limit: None, + temperature: None, + max_tokens: None, + toolshim: false, + toolshim_model: None, fast_model_config: None, - request_params, + request_params: None, reasoning: None, }; config.normalize_effort_suffix(); @@ -186,25 +92,20 @@ impl ModelConfig { } pub fn with_canonical_limits(mut self, provider_name: &str) -> Self { - if let Some(pm) = find_predefined_model(&self.model_name) { - if self.context_limit.is_none() { - self.context_limit = pm.context_limit; - } - } - // Try canonical lookup with the full model name first, then fall back // to the name with reasoning-effort suffixes stripped (e.g. // "databricks-gpt-5.4-high" → "databricks-gpt-5.4"). let canonical = - crate::providers::canonical::maybe_get_canonical_model(provider_name, &self.model_name) - .or_else(|| { + crate::canonical::maybe_get_canonical_model(provider_name, &self.model_name).or_else( + || { let (base, _effort) = extract_reasoning_effort(&self.model_name); if base != self.model_name { - crate::providers::canonical::maybe_get_canonical_model(provider_name, &base) + crate::canonical::maybe_get_canonical_model(provider_name, &base) } else { None } - }); + }, + ); if let Some(canonical) = canonical { if self.context_limit.is_none() { @@ -225,94 +126,6 @@ impl ModelConfig { self } - fn validate_context_limit(val: &str, env_var: &str) -> Result { - let limit = val.parse::().map_err(|_| { - ConfigError::InvalidValue( - env_var.to_string(), - val.to_string(), - "must be a positive integer".to_string(), - ) - })?; - - if limit < 4 * 1024 { - return Err(ConfigError::InvalidRange( - env_var.to_string(), - "must be greater than 4K".to_string(), - )); - } - - Ok(limit) - } - - fn parse_temperature() -> Result, ConfigError> { - if let Ok(val) = std::env::var("GOOSE_TEMPERATURE") { - let temp = val.parse::().map_err(|_| { - ConfigError::InvalidValue( - "GOOSE_TEMPERATURE".to_string(), - val.clone(), - "must be a valid number".to_string(), - ) - })?; - if temp < 0.0 { - return Err(ConfigError::InvalidRange( - "GOOSE_TEMPERATURE".to_string(), - val, - )); - } - Ok(Some(temp)) - } else { - Ok(None) - } - } - - fn parse_max_tokens() -> Result, ConfigError> { - match crate::config::Config::global().get_param::("GOOSE_MAX_TOKENS") { - Ok(tokens) => { - if tokens <= 0 { - return Err(ConfigError::InvalidRange( - "goose_max_tokens".to_string(), - "must be greater than 0".to_string(), - )); - } - Ok(Some(tokens)) - } - Err(crate::config::ConfigError::NotFound(_)) => Ok(None), - Err(e) => Err(ConfigError::InvalidValue( - "goose_max_tokens".to_string(), - String::new(), - e.to_string(), - )), - } - } - - fn parse_toolshim() -> Result { - if let Ok(val) = std::env::var("GOOSE_TOOLSHIM") { - match val.to_lowercase().as_str() { - "1" | "true" | "yes" | "on" => Ok(true), - "0" | "false" | "no" | "off" => Ok(false), - _ => Err(ConfigError::InvalidValue( - "GOOSE_TOOLSHIM".to_string(), - val, - "must be one of: 1, true, yes, on, 0, false, no, off".to_string(), - )), - } - } else { - Ok(false) - } - } - - fn parse_toolshim_model() -> Result, ConfigError> { - match std::env::var("GOOSE_TOOLSHIM_OLLAMA_MODEL") { - Ok(val) if val.trim().is_empty() => Err(ConfigError::InvalidValue( - "GOOSE_TOOLSHIM_OLLAMA_MODEL".to_string(), - val, - "cannot be empty if set".to_string(), - )), - Ok(val) => Ok(Some(val)), - Err(_) => Ok(None), - } - } - pub fn with_context_limit(mut self, limit: Option) -> Self { if limit.is_some() { self.context_limit = limit; @@ -330,6 +143,30 @@ impl ModelConfig { self } + pub fn with_default_context_limit(mut self, limit: Option) -> Self { + if self.context_limit.is_none() { + self.context_limit = limit; + } + + if let Some(fast_config) = self.fast_model_config.take() { + self.fast_model_config = Some(Box::new(fast_config.with_default_context_limit(limit))); + } + + self + } + + pub fn with_default_max_tokens(mut self, tokens: Option) -> Self { + if self.max_tokens.is_none() { + self.max_tokens = tokens; + } + + if let Some(fast_config) = self.fast_model_config.take() { + self.fast_model_config = Some(Box::new(fast_config.with_default_max_tokens(tokens))); + } + + self + } + pub fn with_toolshim(mut self, toolshim: bool) -> Self { self.toolshim = toolshim; self @@ -345,16 +182,16 @@ impl ModelConfig { fast_model_name: &str, provider_name: &str, ) -> Result { - let name = std::env::var("GOOSE_FAST_MODEL") - .ok() - .map(|v| v.trim().to_string()) - .filter(|v| !v.is_empty()) - .unwrap_or_else(|| fast_model_name.to_string()); - let fast_config = ModelConfig::new(&name)?.with_canonical_limits(provider_name); + let fast_config = ModelConfig::new(fast_model_name)?.with_canonical_limits(provider_name); self.fast_model_config = Some(Box::new(fast_config)); Ok(self) } + pub fn with_fast_model_config(mut self, fast_model_config: ModelConfig) -> Self { + self.fast_model_config = Some(Box::new(fast_model_config)); + self + } + pub fn with_merged_request_params(mut self, params: HashMap) -> Self { match self.request_params.as_mut() { Some(existing) => { @@ -378,6 +215,21 @@ impl ModelConfig { self } + pub fn with_default_thinking_effort(mut self, effort: Option) -> Self { + if self.thinking_effort().is_none() { + if let Some(effort) = effort { + self = self.with_thinking_effort(effort); + } + } + + if let Some(fast_config) = self.fast_model_config.take() { + self.fast_model_config = + Some(Box::new(fast_config.with_default_thinking_effort(effort))); + } + + self + } + pub fn with_inherited_session_settings_from( mut self, previous: Option<&ModelConfig>, @@ -482,63 +334,18 @@ impl ModelConfig { } pub fn thinking_effort(&self) -> Option { - self.get_config_param::("thinking_effort", "GOOSE_THINKING_EFFORT") + self.request_param::("thinking_effort") .and_then(|s| s.parse::().ok()) - .or_else(Self::legacy_thinking_effort) } - fn legacy_thinking_effort() -> Option { - let config = crate::config::Config::global(); - - if let Ok(value) = config.get_param::("CLAUDE_THINKING_TYPE") { - if let Some(effort) = match value.to_lowercase().as_str() { - "adaptive" | "enabled" => Some(ThinkingEffort::High), - "disabled" => Some(ThinkingEffort::Off), - _ => None, - } { - return Some(effort); - } - } - - if let Ok(enabled) = config.get_param::("CLAUDE_THINKING_ENABLED") { - return Some(if enabled { - ThinkingEffort::High - } else { - ThinkingEffort::Off - }); - } - - if let Ok(value) = config.get_param::("GEMINI3_THINKING_LEVEL") { - if let Some(effort) = Self::legacy_gemini3_thinking_effort(&value) { - return Some(effort); - } - } - - None - } - - fn legacy_gemini3_thinking_effort(value: &str) -> Option { - match value.to_lowercase().as_str() { - "low" => Some(ThinkingEffort::Low), - "high" => Some(ThinkingEffort::High), - _ => None, - } - } - - pub fn get_config_param serde::Deserialize<'de>>( + pub fn request_param serde::Deserialize<'de>>( &self, request_key: &str, - config_key: &str, ) -> Option { 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::(config_key) - .ok() - }) } pub fn new_or_fail(model_name: &str) -> ModelConfig { @@ -551,131 +358,6 @@ impl ModelConfig { mod tests { use super::*; - #[test] - fn test_parse_max_tokens_valid() { - let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", Some("4096"))]); - let result = ModelConfig::parse_max_tokens().unwrap(); - assert_eq!(result, Some(4096)); - } - - #[test] - fn test_parse_max_tokens_not_set() { - let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", None::<&str>)]); - let result = ModelConfig::parse_max_tokens().unwrap(); - assert_eq!(result, None); - } - - #[test] - fn test_parse_max_tokens_invalid_string() { - let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", Some("not_a_number"))]); - let result = ModelConfig::parse_max_tokens(); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ConfigError::InvalidValue(..))); - } - - #[test] - fn test_parse_max_tokens_zero() { - let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", Some("0"))]); - let result = ModelConfig::parse_max_tokens(); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ConfigError::InvalidRange(..))); - } - - #[test] - fn test_parse_max_tokens_negative() { - let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", Some("-100"))]); - let result = ModelConfig::parse_max_tokens(); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), ConfigError::InvalidRange(..))); - } - - #[test] - fn test_model_config_with_max_tokens_env() { - let _guard = env_lock::lock_env([ - ("GOOSE_MAX_TOKENS", Some("8192")), - ("GOOSE_TEMPERATURE", None::<&str>), - ("GOOSE_CONTEXT_LIMIT", None::<&str>), - ("GOOSE_TOOLSHIM", None::<&str>), - ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), - ]); - let config = ModelConfig::new("test-model").unwrap(); - assert_eq!(config.max_tokens, Some(8192)); - } - - #[test] - fn test_context_limit_from_string_value() { - let _guard = env_lock::lock_env([ - ("GOOSE_MAX_TOKENS", None::<&str>), - ("GOOSE_TEMPERATURE", None::<&str>), - ("GOOSE_CONTEXT_LIMIT", Some("200000")), - ("GOOSE_TOOLSHIM", None::<&str>), - ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), - ]); - let config = ModelConfig::new("test-model").unwrap(); - assert_eq!(config.context_limit, Some(200_000)); - assert_eq!(config.context_limit(), 200_000); - } - - #[test] - fn test_context_limit_invalid_string_value_errors() { - let _guard = env_lock::lock_env([ - ("GOOSE_MAX_TOKENS", None::<&str>), - ("GOOSE_TEMPERATURE", None::<&str>), - ("GOOSE_CONTEXT_LIMIT", Some("not-a-number")), - ("GOOSE_TOOLSHIM", None::<&str>), - ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), - ]); - assert!(ModelConfig::new("test-model").is_err()); - } - - #[test] - fn test_model_config_without_max_tokens_env() { - let _guard = env_lock::lock_env([ - ("GOOSE_MAX_TOKENS", None::<&str>), - ("GOOSE_TEMPERATURE", None::<&str>), - ("GOOSE_CONTEXT_LIMIT", None::<&str>), - ("GOOSE_TOOLSHIM", None::<&str>), - ("GOOSE_TOOLSHIM_OLLAMA_MODEL", None::<&str>), - ]); - 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([("GOOSE_THINKING_EFFORT", Some("high"))]); - - let mut params = HashMap::new(); - params.insert("thinking_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::("thinking_effort", "GOOSE_THINKING_EFFORT"), - Some("low".to_string()) - ); - assert_eq!( - config_without_params - .get_config_param::("thinking_effort", "GOOSE_THINKING_EFFORT"), - Some("high".to_string()) - ); - assert_eq!( - config_without_params - .get_config_param::("nonexistent", "NONEXISTENT_CONFIG_KEY"), - None - ); - } - #[test] fn test_deserialize_preserves_fast_model_config() { let config: ModelConfig = serde_json::from_value(serde_json::json!({ @@ -708,7 +390,6 @@ mod tests { #[test] fn from_request_params() { - let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", None::<&str>)]); let mut params = HashMap::new(); params.insert("thinking_effort".to_string(), serde_json::json!("medium")); let config = ModelConfig { @@ -719,29 +400,6 @@ mod tests { assert_eq!(config.thinking_effort(), Some(ThinkingEffort::Medium)); } - #[test] - fn from_env_var() { - let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", Some("high"))]); - let config = ModelConfig { - model_name: "test".to_string(), - ..Default::default() - }; - assert_eq!(config.thinking_effort(), Some(ThinkingEffort::High)); - } - - #[test] - fn request_params_override_env() { - let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", Some("high"))]); - let mut params = HashMap::new(); - params.insert("thinking_effort".to_string(), serde_json::json!("low")); - let config = ModelConfig { - model_name: "test".to_string(), - request_params: Some(params), - ..Default::default() - }; - assert_eq!(config.thinking_effort(), Some(ThinkingEffort::Low)); - } - #[test] fn with_thinking_effort_sets_request_param() { let config = ModelConfig { @@ -832,22 +490,6 @@ mod tests { assert!(config.request_params.is_none()); } - #[test] - fn does_not_materialize_env_thinking_effort() { - let _guard = env_lock::lock_env([("GOOSE_THINKING_EFFORT", Some("high"))]); - let previous = ModelConfig { - model_name: "previous".to_string(), - ..Default::default() - }; - let config = ModelConfig { - model_name: "next".to_string(), - ..Default::default() - } - .with_inherited_session_settings_from(Some(&previous), None); - - assert!(config.request_params.is_none()); - } - #[test] fn explicit_request_params_override_preserved_session_settings() { let previous = ModelConfig { @@ -879,60 +521,6 @@ mod tests { ); } - #[test] - fn legacy_claude_thinking_type_fallback() { - for value in ["enabled", "adaptive"] { - let _guard = env_lock::lock_env([ - ("GOOSE_THINKING_EFFORT", None::<&str>), - ("CLAUDE_THINKING_TYPE", Some(value)), - ("CLAUDE_THINKING_ENABLED", None::<&str>), - ("GEMINI3_THINKING_LEVEL", None::<&str>), - ("ANTHROPIC_THINKING_BUDGET", None::<&str>), - ("CLAUDE_THINKING_BUDGET", None::<&str>), - ("GEMINI25_THINKING_BUDGET", None::<&str>), - ]); - let config = ModelConfig { - model_name: "test".to_string(), - ..Default::default() - }; - assert_eq!(config.thinking_effort(), Some(ThinkingEffort::High)); - } - } - - #[test] - fn legacy_gemini3_thinking_level_mapping() { - assert_eq!( - ModelConfig::legacy_gemini3_thinking_effort("low"), - Some(ThinkingEffort::Low) - ); - assert_eq!( - ModelConfig::legacy_gemini3_thinking_effort("high"), - Some(ThinkingEffort::High) - ); - assert_eq!(ModelConfig::legacy_gemini3_thinking_effort("auto"), None); - } - - #[test] - fn legacy_gemini3_thinking_level_fallback() { - let temp_dir = tempfile::tempdir().unwrap(); - let temp_root = temp_dir.path().to_string_lossy().to_string(); - let _guard = env_lock::lock_env([ - ("GOOSE_PATH_ROOT", Some(temp_root.as_str())), - ("GOOSE_THINKING_EFFORT", None::<&str>), - ("CLAUDE_THINKING_TYPE", None::<&str>), - ("CLAUDE_THINKING_ENABLED", None::<&str>), - ("GEMINI3_THINKING_LEVEL", Some("high")), - ("ANTHROPIC_THINKING_BUDGET", None::<&str>), - ("CLAUDE_THINKING_BUDGET", None::<&str>), - ("GEMINI25_THINKING_BUDGET", None::<&str>), - ]); - let config = ModelConfig { - model_name: "gemini-3-pro".to_string(), - ..Default::default() - }; - assert_eq!(config.thinking_effort(), Some(ThinkingEffort::High)); - } - #[test] fn effort_suffix_stripped_from_model_name() { let _guard = env_lock::lock_env([ diff --git a/crates/goose-server/src/openapi.rs b/crates/goose-server/src/openapi.rs index e45f3b04d..4610dc815 100644 --- a/crates/goose-server/src/openapi.rs +++ b/crates/goose-server/src/openapi.rs @@ -5,10 +5,10 @@ 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::permission::permission_confirmation::{Permission, PrincipalType}; use goose::providers::base::{ConfigKey, ModelInfo, ProviderMetadata, ProviderType}; use goose::session::{Session, SessionType, SystemInfo}; +use goose_providers::model::ModelConfig; use goose_providers::thinking::ThinkingEffort; use rmcp::model::{ Annotations, Content, EmbeddedResource, Icon, IconTheme, ImageContent, JsonObject, diff --git a/crates/goose-server/src/routes/agent.rs b/crates/goose-server/src/routes/agent.rs index 3df4f5c4a..06cf11652 100644 --- a/crates/goose-server/src/routes/agent.rs +++ b/crates/goose-server/src/routes/agent.rs @@ -19,7 +19,6 @@ use goose::agents::reply_parts::is_tool_visible_to_app; use goose::agents::ExtensionConfig; use goose::config::resolve_extensions_for_new_session; use goose::config::{Config, GooseMode}; -use goose::model::ModelConfig; use goose::providers::create; use goose::recipe::Recipe; use goose::recipe_deeplink; @@ -303,7 +302,9 @@ async fn start_agent( update = update.provider_name(provider); if let Some(ref model) = settings.goose_model { - if let Ok(model_config) = ModelConfig::new(model) { + if let Ok(model_config) = + goose::model_config::model_config_from_user_config(provider, model) + { update = update.model_config(model_config); } } @@ -637,15 +638,15 @@ async fn update_agent_provider( } }; - 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); + let mut model_config = + goose::model_config::model_config_from_user_config(&payload.provider, &model) + .map_err(|e| { + ( + StatusCode::BAD_REQUEST, + format!("Invalid model config: {}", e), + ) + })? + .with_context_limit(payload.context_limit); if let Some(request_params) = payload.request_params { model_config = model_config.with_merged_request_params(request_params); diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index f556c39a6..6cae9dad1 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -13,7 +13,6 @@ use goose::config::paths::Paths; use goose::config::ExtensionEntry; use goose::config::{Config, ConfigError}; use goose::custom_requests::SourceType; -use goose::model::ModelConfig; use goose::providers::base::{ModelInfo, ProviderMetadata, ProviderType}; use goose::providers::canonical::maybe_get_canonical_model; use goose::providers::catalog::{ @@ -27,6 +26,7 @@ use goose::{ agents::execute_commands, agents::ExtensionConfig, config::permission::PermissionLevel, slash_commands::recipe_slash_command, }; +use goose_providers::model::ModelConfig; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_yaml; @@ -937,7 +937,8 @@ pub async fn get_provider_models( ))); } - let model_config = ModelConfig::new(&metadata.default_model)?.with_canonical_limits(&name); + let model_config = + goose::model_config::model_config_from_user_config(&name, &metadata.default_model)?; let provider = goose::providers::create(&name, model_config, Vec::new()).await?; let models_result = provider.fetch_recommended_model_info().await; @@ -971,7 +972,7 @@ pub async fn resolve_provider_model_info( ))); } - let model_config = ModelConfig::new(model)?.with_canonical_limits(name); + let model_config = goose::model_config::model_config_from_user_config(name, model)?; let provider = goose::providers::create(name, model_config.clone(), Vec::new()).await?; match provider.fetch_model_info(model).await { Ok(info) => Ok(info), @@ -1419,7 +1420,6 @@ pub async fn get_provider_catalog_template( pub async fn configure_provider_oauth( Path(provider_name): Path, ) -> Result, ErrorResponse> { - use goose::model::ModelConfig; use goose::providers::create; if !is_valid_provider_name(&provider_name) { @@ -1440,11 +1440,10 @@ pub async fn configure_provider_oauth( return Ok(Json("OAuth configuration completed".to_string())); } - let temp_model = ModelConfig::new("temp") + let temp_model = goose::model_config::model_config_from_user_config(&provider_name, "temp") .map_err(|e| { ErrorResponse::bad_request(format!("Failed to create temporary model config: {}", e)) - })? - .with_canonical_limits(&provider_name); + })?; // OAuth configuration does not use extensions. let provider = create(&provider_name, temp_model, Vec::new()) diff --git a/crates/goose-server/src/routes/errors.rs b/crates/goose-server/src/routes/errors.rs index aa7ffa553..9d3de5a37 100644 --- a/crates/goose-server/src/routes/errors.rs +++ b/crates/goose-server/src/routes/errors.rs @@ -4,8 +4,8 @@ use axum::{ Json, }; use goose::config::ConfigError; -use goose::model::ConfigError as ModelConfigError; use goose_providers::errors::ProviderError; +use goose_providers::model::ConfigError as ModelConfigError; use serde::Serialize; use utoipa::ToSchema; diff --git a/crates/goose/src/acp/provider.rs b/crates/goose/src/acp/provider.rs index 2bce0e561..fe970a10f 100644 --- a/crates/goose/src/acp/provider.rs +++ b/crates/goose/src/acp/provider.rs @@ -34,12 +34,12 @@ use crate::acp::{map_permission_response, PermissionDecision}; use crate::config::{ExtensionConfig, GooseMode}; use crate::context_mgmt::format_message_for_compacting; use crate::conversation::message::{Message, MessageContent, TOOL_META_EXTERNAL_DISPATCH_KEY}; -use crate::model::ModelConfig; use crate::permission::permission_confirmation::PrincipalType; use crate::permission::{Permission, PermissionConfirmation}; use crate::providers::base::{MessageStream, PermissionRouting, Provider}; use crate::subprocess::configure_subprocess; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; /// Sentinel: resolved to the actual model name during connect(). pub const ACP_CURRENT_MODEL: &str = "current"; @@ -1661,7 +1661,7 @@ mod tests { let provider = test_provider(); assert_eq!( provider.get_model_config().context_limit(), - crate::model::DEFAULT_CONTEXT_LIMIT + goose_providers::model::DEFAULT_CONTEXT_LIMIT ); provider.context_size.store(200_000, Ordering::Relaxed); diff --git a/crates/goose/src/acp/response_builder.rs b/crates/goose/src/acp/response_builder.rs index 0201c439e..ffdfe61dd 100644 --- a/crates/goose/src/acp/response_builder.rs +++ b/crates/goose/src/acp/response_builder.rs @@ -1,5 +1,4 @@ -use crate::config::GooseMode; -use crate::model::ModelConfig; +use crate::config::{Config, GooseMode}; use crate::providers::inventory::{ProviderInventoryEntry, ProviderInventoryService}; use crate::session::Session; use agent_client_protocol::schema::{ @@ -9,6 +8,7 @@ use agent_client_protocol::schema::{ SessionNotification, SessionUpdate, UnstructuredCommandInput, }; use agent_client_protocol::{Client, ConnectionTo}; +use goose_providers::model::ModelConfig; use goose_providers::thinking::ThinkingEffort; use strum::{EnumMessage, VariantNames}; @@ -296,6 +296,7 @@ fn current_thinking_effort_value(model_config: &ModelConfig) -> String { if model_config.is_reasoning_model() { model_config .thinking_effort() + .or_else(|| Config::global().get_goose_thinking_effort()) .map(|effort| effort.to_string()) .unwrap_or_else(|| "off".to_string()) } else { diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index f65d833fc..1133c3cdf 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -100,7 +100,7 @@ mod tools; pub type AcpProviderFactory = Arc< dyn Fn( String, - crate::model::ModelConfig, + goose_providers::model::ModelConfig, Vec, Option, ) -> BoxFuture<'static, Result>> @@ -399,7 +399,7 @@ fn push_or_replace_extension(extensions: &mut Vec, extension: E fn resolve_default_provider_model_config( config: &Config, -) -> Result<(String, crate::model::ModelConfig), agent_client_protocol::Error> { +) -> Result<(String, goose_providers::model::ModelConfig), agent_client_protocol::Error> { let resolved_provider = config.get_goose_provider().map_err(|error| { agent_client_protocol::Error::internal_error() .data(format!("Failed to resolve provider: {}", error)) @@ -408,30 +408,32 @@ fn resolve_default_provider_model_config( agent_client_protocol::Error::internal_error() .data(format!("Failed to resolve model: {}", error)) })?; - let resolved_model_config = crate::model::ModelConfig::new(&resolved_model) - .map(|model_config| model_config.with_canonical_limits(&resolved_provider)) - .map_err(|error| { - agent_client_protocol::Error::internal_error() - .data(format!("Failed to resolve model: {}", error)) - })?; + let resolved_model_config = + crate::model_config::model_config_from_user_config(&resolved_provider, &resolved_model) + .map_err(|error| { + agent_client_protocol::Error::internal_error() + .data(format!("Failed to resolve model: {}", error)) + })?; Ok((resolved_provider, resolved_model_config)) } async fn resolve_provider_default_model_config( provider_name: &str, -) -> Result { +) -> Result { let entry = crate::providers::get_from_registry(provider_name) .await .map_err(|error| { agent_client_protocol::Error::invalid_params() .data(format!("Unknown provider '{}': {}", provider_name, error)) })?; - crate::model::ModelConfig::new(&entry.metadata().default_model) - .map(|model_config| model_config.with_canonical_limits(provider_name)) - .map_err(|error| { - agent_client_protocol::Error::internal_error() - .data(format!("Failed to resolve model: {}", error)) - }) + crate::model_config::model_config_from_user_config( + provider_name, + &entry.metadata().default_model, + ) + .map_err(|error| { + agent_client_protocol::Error::internal_error() + .data(format!("Failed to resolve model: {}", error)) + }) } fn get_requested_line(arguments: Option<&rmcp::model::JsonObject>) -> Option { @@ -912,7 +914,7 @@ impl GooseAcpAgent { async fn create_provider( &self, provider_name: &str, - model_config: crate::model::ModelConfig, + model_config: goose_providers::model::ModelConfig, extensions: Vec, working_dir: Option, ) -> Result> { @@ -2694,11 +2696,15 @@ impl GooseAcpAgent { .internal_err_ctx("Failed to get provider")?; let provider_name = current_provider.get_name().to_string(); let current_model_config = current_provider.get_model_config(); - let model_config = crate::model::ModelConfig::new(model_id) - .invalid_params_err_ctx("Invalid model config")? - .with_canonical_limits(&provider_name); let model_config = - model_config.with_inherited_session_settings_from(Some(¤t_model_config), None); + crate::model_config::model_config_from_user_config_with_session_settings( + &provider_name, + model_id, + Some(¤t_model_config), + None, + None, + ) + .invalid_params_err_ctx("Invalid model config")?; agent .recreate_provider_for_session(session_id, &provider_name, model_config) .await @@ -2830,12 +2836,15 @@ impl GooseAcpAgent { current_model }; let model = model_name.unwrap_or(&default_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); - model_config = model_config - .with_inherited_session_settings_from(Some(¤t_model_config), request_params); + let model_config = + crate::model_config::model_config_from_user_config_with_session_settings( + &resolved_provider_name, + model, + Some(¤t_model_config), + request_params, + context_limit, + ) + .invalid_params_err_ctx("Invalid model config")?; agent .recreate_provider_for_session(session_id, &resolved_provider_name, model_config) @@ -3748,7 +3757,7 @@ print(\"hello, world\") fn test_build_usage_update_clamps_negative_used_to_zero() { let mut session = make_session_with_usage(Some(-7), Some(0), Some(0), None, None, None); session.model_config = Some( - crate::model::ModelConfig::new("test-model") + goose_providers::model::ModelConfig::new("test-model") .unwrap() .with_context_limit(Some(258_000)), ); diff --git a/crates/goose/src/acp/server/providers.rs b/crates/goose/src/acp/server/providers.rs index c24105fb0..fc5f9671c 100644 --- a/crates/goose/src/acp/server/providers.rs +++ b/crates/goose/src/acp/server/providers.rs @@ -446,8 +446,11 @@ impl GooseAcpAgent { let entry = crate::providers::get_from_registry(&req.provider_id) .await .invalid_params_err_ctx("Unknown provider")?; - let model_config = crate::model::ModelConfig::new(&entry.metadata().default_model) - .invalid_params_err_ctx("Invalid default model")?; + let model_config = crate::model_config::model_config_from_user_config( + &req.provider_id, + &entry.metadata().default_model, + ) + .invalid_params_err_ctx("Invalid default model")?; let provider = self .create_provider(&req.provider_id, model_config, Vec::new(), None) .await @@ -720,9 +723,10 @@ impl GooseAcpAgent { let mut refresh_guard = provider_inventory.refresh_guard(&identity); let provider_result = AssertUnwindSafe(async { let metadata = crate::providers::get_from_registry(&provider_id).await?; - let model_config = - crate::model::ModelConfig::new(&metadata.metadata().default_model)? - .with_canonical_limits(&provider_id); + let model_config = crate::model_config::model_config_from_user_config( + &provider_id, + &metadata.metadata().default_model, + )?; provider_factory(provider_id.clone(), model_config, Vec::new(), None).await }) .catch_unwind() diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 02df190d0..bc19b12ad 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -707,7 +707,7 @@ impl Agent { .provider() .await .map(|p| p.get_model_config().context_limit()) - .unwrap_or(crate::model::DEFAULT_CONTEXT_LIMIT); + .unwrap_or(goose_providers::model::DEFAULT_CONTEXT_LIMIT); let compaction_threshold = Config::global() .get_param::("GOOSE_AUTO_COMPACT_THRESHOLD") .unwrap_or(crate::context_mgmt::DEFAULT_COMPACTION_THRESHOLD); @@ -2651,7 +2651,7 @@ impl Agent { &self, session_id: &str, provider_name: &str, - model_config: crate::model::ModelConfig, + model_config: goose_providers::model::ModelConfig, ) -> Result<()> { let session = self .config @@ -2714,9 +2714,8 @@ impl Agent { .get_goose_model() .ok() .ok_or_else(|| anyhow!("Could not configure agent: missing model"))?; - crate::model::ModelConfig::new(&model_name) + crate::model_config::model_config_from_user_config(&provider_name, &model_name) .map_err(|e| anyhow!("Could not configure agent: invalid model {}", e))? - .with_canonical_limits(&provider_name) } }; @@ -2758,9 +2757,11 @@ impl Agent { .get_goose_model() .ok() .ok_or_else(|| anyhow!("Could not configure fallback provider: missing model"))?; - let fallback_model_config = crate::model::ModelConfig::new(&fallback_model_name) - .map_err(|e| anyhow!("Could not configure fallback provider: invalid model {}", e))? - .with_canonical_limits(&fallback_provider_name); + let fallback_model_config = crate::model_config::model_config_from_user_config( + &fallback_provider_name, + &fallback_model_name, + ) + .map_err(|e| anyhow!("Could not configure fallback provider: invalid model {}", e))?; let fallback_provider = crate::providers::create_with_working_dir( &fallback_provider_name, @@ -3189,12 +3190,12 @@ mod tests { fn get_name(&self) -> &str { "test-action-required" } - fn get_model_config(&self) -> crate::model::ModelConfig { - crate::model::ModelConfig::new("test").unwrap() + fn get_model_config(&self) -> goose_providers::model::ModelConfig { + goose_providers::model::ModelConfig::new("test").unwrap() } async fn stream( &self, - _: &crate::model::ModelConfig, + _: &goose_providers::model::ModelConfig, _: &str, _: &str, _: &[crate::conversation::message::Message], @@ -3376,7 +3377,7 @@ exit 0 impl crate::providers::base::Provider for CountingTextProvider { async fn stream( &self, - _model_config: &crate::model::ModelConfig, + _model_config: &goose_providers::model::ModelConfig, _session_id: &str, _system_prompt: &str, _messages: &[Message], @@ -3388,8 +3389,8 @@ exit 0 Ok(stream_from_single_message(message, usage)) } - fn get_model_config(&self) -> crate::model::ModelConfig { - crate::model::ModelConfig::new("mock-model").unwrap() + fn get_model_config(&self) -> goose_providers::model::ModelConfig { + goose_providers::model::ModelConfig::new("mock-model").unwrap() } fn get_name(&self) -> &str { @@ -3405,7 +3406,7 @@ exit 0 impl crate::providers::base::Provider for RefusingProvider { async fn stream( &self, - _model_config: &crate::model::ModelConfig, + _model_config: &goose_providers::model::ModelConfig, _session_id: &str, _system_prompt: &str, _messages: &[Message], @@ -3420,8 +3421,8 @@ exit 0 }))) } - fn get_model_config(&self) -> crate::model::ModelConfig { - crate::model::ModelConfig::new("mock-model").unwrap() + fn get_model_config(&self) -> goose_providers::model::ModelConfig { + goose_providers::model::ModelConfig::new("mock-model").unwrap() } fn get_name(&self) -> &str { diff --git a/crates/goose/src/agents/platform_extensions/summon.rs b/crates/goose/src/agents/platform_extensions/summon.rs index 64a576cda..af22aeef5 100644 --- a/crates/goose/src/agents/platform_extensions/summon.rs +++ b/crates/goose/src/agents/platform_extensions/summon.rs @@ -1587,10 +1587,9 @@ impl SummonClient { recipe: &Recipe, session: &crate::session::Session, provider_name: &str, - ) -> Result { + ) -> Result { let mut model_config = session.model_config.clone().map(Ok).unwrap_or_else(|| { - crate::model::ModelConfig::new("default") - .map(|c| c.with_canonical_limits(provider_name)) + crate::model_config::model_config_from_user_config(provider_name, "default") })?; let override_model = params @@ -1612,7 +1611,7 @@ impl SummonClient { // not model-specific from the parent. let parent = model_config; let mut cfg = - crate::model::ModelConfig::new(&model)?.with_canonical_limits(provider_name); + crate::model_config::model_config_from_user_config(provider_name, &model)?; cfg.toolshim = parent.toolshim; cfg.toolshim_model = parent.toolshim_model; cfg.fast_model_config = parent.fast_model_config; @@ -2555,7 +2554,7 @@ You review code."#; const OVERRIDE_MODEL: &str = "claude-opus-4-6"; const PROVIDER: &str = "anthropic"; - fn session_with(parent: crate::model::ModelConfig) -> crate::session::Session { + fn session_with(parent: goose_providers::model::ModelConfig) -> crate::session::Session { crate::session::Session { provider_name: Some(PROVIDER.to_string()), model_config: Some(parent), @@ -2565,8 +2564,8 @@ You review code."#; fn resolve_with_override( model: Option<&str>, - parent: crate::model::ModelConfig, - ) -> crate::model::ModelConfig { + parent: goose_providers::model::ModelConfig, + ) -> goose_providers::model::ModelConfig { let client = SummonClient::new(create_test_context()).unwrap(); let params = DelegateParams { model: model.map(String::from), @@ -2577,8 +2576,8 @@ You review code."#; .expect("resolve_model_config") } - fn parent_config() -> crate::model::ModelConfig { - crate::model::ModelConfig::new(PARENT_MODEL) + fn parent_config() -> goose_providers::model::ModelConfig { + goose_providers::model::ModelConfig::new(PARENT_MODEL) .unwrap() .with_canonical_limits(PROVIDER) } @@ -2593,7 +2592,7 @@ You review code."#; ]); let parent = parent_config(); - let overridden = crate::model::ModelConfig::new(OVERRIDE_MODEL) + let overridden = goose_providers::model::ModelConfig::new(OVERRIDE_MODEL) .unwrap() .with_canonical_limits(PROVIDER); assert_ne!(parent.context_limit, overridden.context_limit); diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 4f123a83c..0a3873179 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -11,6 +11,7 @@ use tracing::debug; use super::super::agents::Agent; #[cfg(feature = "code-mode")] use crate::agents::platform_extensions::code_execution; +use crate::config::Config; use crate::conversation::message::{Message, MessageContent, ToolRequest}; use crate::conversation::Conversation; #[cfg(test)] @@ -280,7 +281,9 @@ impl Agent { // Capture errors during stream creation and return them as part of the stream // so they can be handled by the existing error handling logic in the agent - let model_config = provider.get_model_config(); + let model_config = provider + .get_model_config() + .with_default_thinking_effort(Config::global().get_goose_thinking_effort()); debug!("WAITING_LLM_STREAM_START"); let stream_result = provider .stream( @@ -636,11 +639,11 @@ mod tests { use super::*; use crate::config::GooseMode; use crate::conversation::message::Message; - use crate::model::ModelConfig; use crate::providers::base::Provider; use crate::session::session_manager::SessionType; use async_trait::async_trait; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; + use goose_providers::model::ModelConfig; use rmcp::object; #[derive(Clone)] diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index f26d9bdab..81585e7ad 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1,6 +1,7 @@ use crate::config::paths::Paths; use crate::config::GooseMode; use fs2::FileExt; +use goose_providers::thinking::ThinkingEffort; #[cfg(feature = "system-keyring")] use keyring::Entry; use once_cell::sync::OnceCell; @@ -1136,7 +1137,78 @@ config_value!(GOOSE_PROMPT_EDITOR_ALWAYS, Option); config_value!(GOOSE_MAX_ACTIVE_AGENTS, usize); config_value!(GOOSE_DISABLE_SESSION_NAMING, bool); config_value!(GOOSE_DISABLE_TOOL_CALL_SUMMARY, bool); -config_value!(GOOSE_THINKING_EFFORT, String); + +impl Config { + pub fn get_goose_context_limit(&self) -> Result, ConfigError> { + match self.get_param::("GOOSE_CONTEXT_LIMIT") { + Ok(0) => Err(ConfigError::DeserializeError( + "GOOSE_CONTEXT_LIMIT must be greater than 0".to_string(), + )), + Ok(limit) => Ok(Some(limit)), + Err(ConfigError::NotFound(_)) => Ok(None), + Err(e) => Err(e), + } + } + + pub fn get_goose_max_tokens(&self) -> Result, ConfigError> { + match self.get_param::("GOOSE_MAX_TOKENS") { + Ok(tokens) if tokens <= 0 => Err(ConfigError::DeserializeError( + "GOOSE_MAX_TOKENS must be greater than 0".to_string(), + )), + Ok(tokens) => Ok(Some(tokens)), + Err(ConfigError::NotFound(_)) => Ok(None), + Err(e) => Err(e), + } + } + + pub fn get_goose_thinking_effort(&self) -> Option { + self.get_param::("GOOSE_THINKING_EFFORT") + .ok() + .and_then(|e| e.parse().ok()) + .or_else(|| self.legacy_thinking_effort()) + } + + pub fn set_goose_thinking_effort(&self, v: ThinkingEffort) -> Result<(), ConfigError> { + self.set_param("GOOSE_THINKING_EFFORT", v) + } + + fn legacy_thinking_effort(&self) -> Option { + if let Ok(value) = self.get_param::("CLAUDE_THINKING_TYPE") { + if let Some(effort) = match value.to_lowercase().as_str() { + "adaptive" | "enabled" => Some(ThinkingEffort::High), + "disabled" => Some(ThinkingEffort::Off), + _ => None, + } { + return Some(effort); + } + } + + if let Ok(enabled) = self.get_param::("CLAUDE_THINKING_ENABLED") { + return Some(if enabled { + ThinkingEffort::High + } else { + ThinkingEffort::Off + }); + } + + if let Ok(value) = self.get_param::("GEMINI3_THINKING_LEVEL") { + if let Some(effort) = Self::legacy_gemini3_thinking_effort(&value) { + return Some(effort); + } + } + + None + } + + fn legacy_gemini3_thinking_effort(value: &str) -> Option { + match value.to_lowercase().as_str() { + "low" => Some(ThinkingEffort::Low), + "high" => Some(ThinkingEffort::High), + _ => None, + } + } +} + config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64); fn find_workspace_or_exe_root() -> Option { @@ -2398,4 +2470,132 @@ extensions: assert!(openai.get("enabled").unwrap().as_bool().unwrap()); assert!(openai.get("configured").unwrap().as_bool().unwrap()); } + + #[test] + fn get_goose_context_limit_reads_env() { + let _guard = env_lock::lock_env([("GOOSE_CONTEXT_LIMIT", Some("4096"))]); + let config = new_test_config(); + + assert_eq!(config.get_goose_context_limit().unwrap(), Some(4096)); + } + + #[test] + fn get_goose_context_limit_reads_quoted_yaml_value() { + let _guard = env_lock::lock_env([("GOOSE_CONTEXT_LIMIT", None::<&str>)]); + let config = new_test_config(); + config.set_param("GOOSE_CONTEXT_LIMIT", "200000").unwrap(); + + assert_eq!(config.get_goose_context_limit().unwrap(), Some(200_000)); + } + + #[test] + fn get_goose_context_limit_returns_none_when_not_set() { + let _guard = env_lock::lock_env([("GOOSE_CONTEXT_LIMIT", None::<&str>)]); + let config = new_test_config(); + + assert_eq!(config.get_goose_context_limit().unwrap(), None); + } + + #[test] + fn get_goose_context_limit_rejects_zero() { + let _guard = env_lock::lock_env([("GOOSE_CONTEXT_LIMIT", Some("0"))]); + let config = new_test_config(); + + assert!(matches!( + config.get_goose_context_limit().unwrap_err(), + ConfigError::DeserializeError(_) + )); + } + + #[test] + fn get_goose_max_tokens_reads_env() { + let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", Some("4096"))]); + let config = new_test_config(); + + assert_eq!(config.get_goose_max_tokens().unwrap(), Some(4096)); + } + + #[test] + fn get_goose_max_tokens_returns_none_when_not_set() { + let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", None::<&str>)]); + let config = new_test_config(); + + assert_eq!(config.get_goose_max_tokens().unwrap(), None); + } + + #[test] + fn get_goose_max_tokens_rejects_invalid_values() { + for value in ["not_a_number", "0", "-100"] { + let _guard = env_lock::lock_env([("GOOSE_MAX_TOKENS", Some(value))]); + let config = new_test_config(); + + assert!(matches!( + config.get_goose_max_tokens().unwrap_err(), + ConfigError::DeserializeError(_) + )); + } + } + + #[test] + fn get_goose_thinking_effort_reads_env() { + let _guard = env_lock::lock_env([ + ("GOOSE_THINKING_EFFORT", Some("high")), + ("CLAUDE_THINKING_TYPE", None::<&str>), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ("GEMINI3_THINKING_LEVEL", None::<&str>), + ]); + let config = new_test_config(); + + assert_eq!( + config.get_goose_thinking_effort(), + Some(ThinkingEffort::High) + ); + } + + #[test] + fn get_goose_thinking_effort_uses_legacy_claude_fallback() { + for value in ["enabled", "adaptive"] { + let _guard = env_lock::lock_env([ + ("GOOSE_THINKING_EFFORT", None::<&str>), + ("CLAUDE_THINKING_TYPE", Some(value)), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ("GEMINI3_THINKING_LEVEL", None::<&str>), + ]); + let config = new_test_config(); + + assert_eq!( + config.get_goose_thinking_effort(), + Some(ThinkingEffort::High) + ); + } + } + + #[test] + fn get_goose_thinking_effort_uses_legacy_gemini3_fallback() { + let _guard = env_lock::lock_env([ + ("GOOSE_THINKING_EFFORT", None::<&str>), + ("CLAUDE_THINKING_TYPE", None::<&str>), + ("CLAUDE_THINKING_ENABLED", None::<&str>), + ("GEMINI3_THINKING_LEVEL", Some("high")), + ]); + let config = new_test_config(); + + assert_eq!( + config.get_goose_thinking_effort(), + Some(ThinkingEffort::High) + ); + } + + #[test] + fn legacy_gemini3_thinking_level_mapping() { + assert_eq!( + Config::legacy_gemini3_thinking_effort("low"), + Some(ThinkingEffort::Low) + ); + assert_eq!( + Config::legacy_gemini3_thinking_effort("high"), + Some(ThinkingEffort::High) + ); + assert_eq!(Config::legacy_gemini3_thinking_effort("auto"), None); + } } diff --git a/crates/goose/src/context_mgmt/mod.rs b/crates/goose/src/context_mgmt/mod.rs index dbae784b4..1cc4173db 100644 --- a/crates/goose/src/context_mgmt/mod.rs +++ b/crates/goose/src/context_mgmt/mod.rs @@ -567,10 +567,10 @@ pub fn maybe_summarize_tool_pairs( #[cfg(test)] mod tests { use super::*; - use crate::model::ModelConfig; use async_trait::async_trait; use goose_providers::conversation::token_usage::Usage; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; use rmcp::model::{AnnotateAble, CallToolRequestParams, RawContent, Tool}; fn create_tool_pair( diff --git a/crates/goose/src/doctor.rs b/crates/goose/src/doctor.rs index 2fadd576d..5d9624682 100644 --- a/crates/goose/src/doctor.rs +++ b/crates/goose/src/doctor.rs @@ -4,7 +4,6 @@ use crate::agents::platform_extensions::developer; use crate::agents::ExtensionConfig; use crate::config::Config; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers; use crate::providers::base::Provider; use crate::session::{ @@ -168,9 +167,9 @@ async fn try_create_and_test( provider_name: &str, model_name: &str, ) -> Result, ProviderError> { - let model_config = ModelConfig::new(model_name) - .map_err(|e| ProviderError::ExecutionError(e.to_string()))? - .with_canonical_limits(provider_name); + let model_config = + crate::model_config::model_config_from_user_config(provider_name, model_name) + .map_err(|e| ProviderError::ExecutionError(e.to_string()))?; let provider = providers::create(provider_name, model_config, vec![]) .await diff --git a/crates/goose/src/execution/manager.rs b/crates/goose/src/execution/manager.rs index 0e4604bc7..68ce714da 100644 --- a/crates/goose/src/execution/manager.rs +++ b/crates/goose/src/execution/manager.rs @@ -609,10 +609,10 @@ mod tests { use rmcp::model::Tool; use crate::conversation::message::Message; - use crate::model::ModelConfig; use crate::providers::base::{MessageStream, Provider}; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; struct FailingProvider; diff --git a/crates/goose/src/gateway/handler.rs b/crates/goose/src/gateway/handler.rs index 31fdde38c..bbe1ecf3c 100644 --- a/crates/goose/src/gateway/handler.rs +++ b/crates/goose/src/gateway/handler.rs @@ -11,7 +11,6 @@ use crate::config::paths::Paths; use crate::config::Config; use crate::conversation::message::{Message, MessageContent}; use crate::execution::manager::AgentManager; -use crate::model::ModelConfig; use crate::session::SessionType; use crate::session::{EnabledExtensionsState, ExtensionState, Session}; @@ -164,11 +163,14 @@ impl GatewayHandler { // Store the current provider and model config on the session so the agent // can be restored after LRU eviction, matching the start_agent flow. let mut update = manager.update(&session.id); - if let Ok(provider) = config.get_goose_provider() { + let provider = config.get_goose_provider().ok(); + if let Some(ref provider) = provider { update = update.provider_name(provider); } - if let Ok(model_name) = config.get_goose_model() { - if let Ok(model_config) = ModelConfig::new(&model_name) { + if let (Some(ref provider), Ok(model_name)) = (&provider, config.get_goose_model()) { + if let Ok(model_config) = + crate::model_config::model_config_from_user_config(provider, &model_name) + { update = update.model_config(model_config); } } @@ -259,8 +261,11 @@ impl GatewayHandler { if let Some(ref provider) = current_provider { update = update.provider_name(provider); } - if let Some(ref model_name) = current_model_name { - if let Ok(model_config) = ModelConfig::new(model_name) { + if let (Some(ref provider), Some(ref model_name)) = (¤t_provider, ¤t_model_name) + { + if let Ok(model_config) = + crate::model_config::model_config_from_user_config(provider, model_name) + { update = update.model_config(model_config); } } diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 8ac452073..86805ffa1 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -24,7 +24,7 @@ pub mod hooks; pub mod instance_id; pub mod logging; pub mod mcp_utils; -pub mod model; +pub mod model_config; pub mod oauth; #[cfg(feature = "otel")] pub mod otel; diff --git a/crates/goose/src/model_config.rs b/crates/goose/src/model_config.rs new file mode 100644 index 000000000..779ab3e9a --- /dev/null +++ b/crates/goose/src/model_config.rs @@ -0,0 +1,150 @@ +use crate::config::{Config, ConfigError}; +use anyhow::{anyhow, Result}; +use goose_providers::model::ModelConfig; +use serde_json::Value; +use std::collections::HashMap; + +pub fn model_config_from_user_config( + provider_name: &str, + model_name: impl AsRef, +) -> Result { + let model = base_model_config_from_user_config(model_name.as_ref())?; + materialize_model_config(provider_name, model) +} + +pub fn model_config_from_user_config_with_session_settings( + provider_name: &str, + model_name: impl AsRef, + previous: Option<&ModelConfig>, + request_params: Option>, + context_limit: Option, +) -> Result { + let config = Config::global(); + let model = base_model_config_from_user_config(model_name.as_ref())?; + let model = materialize_model_config_inner(model, false)? + .with_context_limit(context_limit) + .with_inherited_session_settings_from(previous, request_params) + .with_default_thinking_effort(config.get_goose_thinking_effort()); + + Ok(model.with_canonical_limits(provider_name)) +} + +pub fn materialize_model_config(provider_name: &str, model: ModelConfig) -> Result { + let model = materialize_model_config_inner(model, true)?; + Ok(model.with_canonical_limits(provider_name)) +} + +fn materialize_model_config_inner( + mut model: ModelConfig, + include_default_thinking_effort: bool, +) -> Result { + let config = Config::global(); + + if model.temperature.is_none() { + model = model.with_temperature(get_goose_temperature(config)?); + } + + if model.toolshim && model.toolshim_model.is_none() { + model = model.with_toolshim_model(get_goose_toolshim_model(config)?); + } + + model = model + .with_default_context_limit(config.get_goose_context_limit()?) + .with_default_max_tokens(config.get_goose_max_tokens()?); + + if include_default_thinking_effort { + model = model.with_default_thinking_effort(config.get_goose_thinking_effort()); + } + + Ok(model) +} + +pub fn configured_fast_model_name(default_model: &str) -> String { + Config::global() + .get_param::("GOOSE_FAST_MODEL") + .ok() + .map(|v| v.trim().to_string()) + .filter(|v| !v.is_empty()) + .unwrap_or_else(|| default_model.to_string()) +} + +pub fn with_configured_fast_model( + model: ModelConfig, + provider_name: &str, + default_fast_model_name: &str, +) -> Result { + let fast_model_name = configured_fast_model_name(default_fast_model_name); + let fast_model_config = model_config_from_user_config(provider_name, fast_model_name)?; + Ok(model.with_fast_model_config(fast_model_config)) +} + +fn base_model_config_from_user_config(model_name: &str) -> Result { + let config = Config::global(); + let mut model = ModelConfig { + model_name: model_name.to_string(), + context_limit: None, + temperature: get_goose_temperature(config)?, + max_tokens: None, + toolshim: get_goose_toolshim(config)?.unwrap_or(false), + toolshim_model: get_goose_toolshim_model(config)?, + fast_model_config: None, + request_params: None, + reasoning: None, + }; + model.normalize_effort_suffix(); + Ok(model) +} + +fn get_goose_temperature(config: &Config) -> Result> { + match config.get_param::("GOOSE_TEMPERATURE") { + Ok(temp) if temp < 0.0 => Err(anyhow!( + "Value for 'GOOSE_TEMPERATURE' is out of valid range: {temp}" + )), + Ok(temp) => Ok(Some(temp)), + Err(ConfigError::NotFound(_)) => Ok(None), + Err(e) => Err(e.into()), + } +} + +fn get_goose_toolshim(config: &Config) -> Result> { + match config.get_param::("GOOSE_TOOLSHIM") { + Ok(value) => parse_yaml_bool_config("GOOSE_TOOLSHIM", value).map(Some), + Err(ConfigError::NotFound(_)) => Ok(None), + Err(e) => Err(e.into()), + } +} + +fn get_goose_toolshim_model(config: &Config) -> Result> { + match config.get_param::("GOOSE_TOOLSHIM_OLLAMA_MODEL") { + Ok(value) if value.trim().is_empty() => Err(anyhow!( + "Invalid value for 'GOOSE_TOOLSHIM_OLLAMA_MODEL': '{value}' - cannot be empty if set" + )), + Ok(value) => Ok(Some(value)), + Err(ConfigError::NotFound(_)) => Ok(None), + Err(e) => Err(e.into()), + } +} + +fn parse_bool_config(key: &str, value: &str) -> Result { + match value.to_lowercase().as_str() { + "1" | "true" | "yes" | "on" => Ok(true), + "0" | "false" | "no" | "off" => Ok(false), + _ => Err(anyhow!( + "Invalid value for '{key}': '{value}' - must be one of: 1, true, yes, on, 0, false, no, off" + )), + } +} + +fn parse_yaml_bool_config(key: &str, value: serde_yaml::Value) -> Result { + match value { + serde_yaml::Value::Bool(value) => Ok(value), + serde_yaml::Value::Number(value) => parse_bool_config(key, &value.to_string()), + serde_yaml::Value::String(value) => parse_bool_config(key, &value), + other => { + Err(anyhow!( + "Invalid value for '{key}': '{}' - must be one of: 1, true, yes, on, 0, false, no, off", + serde_yaml::to_string(&other).unwrap_or_else(|_| "".to_string()).trim() + )) + } + } +} diff --git a/crates/goose/src/providers/amp_acp.rs b/crates/goose/src/providers/amp_acp.rs index cfeead29d..efa625094 100644 --- a/crates/goose/src/providers/amp_acp.rs +++ b/crates/goose/src/providers/amp_acp.rs @@ -8,8 +8,8 @@ use crate::acp::{ }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; -use crate::model::ModelConfig; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; +use goose_providers::model::ModelConfig; pub(crate) const AMP_ACP_PROVIDER_NAME: &str = "amp-acp"; const AMP_ACP_DOC_URL: &str = "https://ampcode.com"; diff --git a/crates/goose/src/providers/anthropic.rs b/crates/goose/src/providers/anthropic.rs index f8494188c..d5fc4627f 100644 --- a/crates/goose/src/providers/anthropic.rs +++ b/crates/goose/src/providers/anthropic.rs @@ -20,9 +20,9 @@ use super::openai_compatible::map_http_error_to_provider_error; use super::retry::ProviderRetry; use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::utils::RequestLog; use futures::future::BoxFuture; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; pub const ANTHROPIC_DEFAULT_MODEL: &str = "claude-sonnet-4-5"; @@ -64,7 +64,11 @@ pub struct AnthropicProvider { impl AnthropicProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(ANTHROPIC_DEFAULT_FAST_MODEL, ANTHROPIC_PROVIDER_NAME)?; + let model = crate::model_config::with_configured_fast_model( + model, + ANTHROPIC_PROVIDER_NAME, + ANTHROPIC_DEFAULT_FAST_MODEL, + )?; let config = crate::config::Config::global(); let api_key: String = config.get_secret("ANTHROPIC_API_KEY")?; @@ -151,7 +155,7 @@ impl AnthropicProvider { } let model = if let Some(ref fast_model_name) = config.fast_model { - model.with_fast(fast_model_name, &config.name)? + crate::model_config::with_configured_fast_model(model, &config.name, fast_model_name)? } else { model }; diff --git a/crates/goose/src/providers/avian.rs b/crates/goose/src/providers/avian.rs index 6efcd1f12..3a92a5dc5 100644 --- a/crates/goose/src/providers/avian.rs +++ b/crates/goose/src/providers/avian.rs @@ -1,9 +1,9 @@ use super::api_client::{ApiClient, AuthMethod}; use super::base::{ConfigKey, ProviderDef, ProviderMetadata}; use super::openai_compatible::OpenAiCompatibleProvider; -use crate::model::ModelConfig; use anyhow::Result; use futures::future::BoxFuture; +use goose_providers::model::ModelConfig; const AVIAN_PROVIDER_NAME: &str = "avian"; pub const AVIAN_API_HOST: &str = "https://api.avian.io/v1"; diff --git a/crates/goose/src/providers/azure.rs b/crates/goose/src/providers/azure.rs index 6602f5751..161bb28bf 100644 --- a/crates/goose/src/providers/azure.rs +++ b/crates/goose/src/providers/azure.rs @@ -5,8 +5,8 @@ use super::api_client::{ApiClient, AuthMethod, AuthProvider}; use super::azureauth::{AuthError, AzureAuth}; use super::base::{ConfigKey, ProviderDef, ProviderMetadata}; use super::openai_compatible::OpenAiCompatibleProvider; -use crate::model::ModelConfig; use futures::future::BoxFuture; +use goose_providers::model::ModelConfig; const AZURE_PROVIDER_NAME: &str = "azure_openai"; pub const AZURE_DEFAULT_MODEL: &str = "gpt-4o"; diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index c6db8dec9..27868f5ad 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -20,9 +20,9 @@ use crate::config::base::ConfigValue; use crate::config::{ExtensionConfig, GooseMode}; use crate::conversation::message::{Message, MessageContent}; use crate::conversation::Conversation; -use crate::model::ModelConfig; use crate::permission::PermissionConfirmation; use crate::utils::safe_truncate; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use utoipa::ToSchema; diff --git a/crates/goose/src/providers/bedrock.rs b/crates/goose/src/providers/bedrock.rs index 5e130549b..b8bcac549 100644 --- a/crates/goose/src/providers/bedrock.rs +++ b/crates/goose/src/providers/bedrock.rs @@ -5,7 +5,6 @@ use super::formats::openai_responses::create_responses_request; use super::openai_compatible::{handle_status, stream_responses_compat}; use super::retry::{ProviderRetry, RetryConfig}; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::utils::RequestLog; use crate::session_context::SESSION_ID_HEADER; use anyhow::Result; @@ -21,6 +20,7 @@ use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::extract_reasoning_effort; +use goose_providers::model::ModelConfig; use reqwest::header::{HeaderName, HeaderValue, AUTHORIZATION}; use rmcp::model::{object, CallToolRequestParams, ErrorCode, ErrorData, Tool}; use serde_json::Value; diff --git a/crates/goose/src/providers/chatgpt_codex.rs b/crates/goose/src/providers/chatgpt_codex.rs index 8d8731ba3..9b0479d7c 100644 --- a/crates/goose/src/providers/chatgpt_codex.rs +++ b/crates/goose/src/providers/chatgpt_codex.rs @@ -1,6 +1,5 @@ use crate::config::paths::Paths; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::providers::api_client::AuthProvider; use crate::providers::base::{ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata}; use crate::providers::formats::openai_responses::responses_api_to_streaming_message; @@ -16,6 +15,7 @@ use chrono::{DateTime, Utc}; use futures::future::BoxFuture; use futures::{StreamExt, TryStreamExt}; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use jsonwebtoken::jwk::JwkSet; use jsonwebtoken::{decode, decode_header, DecodingKey, Validation}; use reqwest::header::{HeaderName, HeaderValue}; diff --git a/crates/goose/src/providers/claude_acp.rs b/crates/goose/src/providers/claude_acp.rs index d4c6c11c7..32c70d251 100644 --- a/crates/goose/src/providers/claude_acp.rs +++ b/crates/goose/src/providers/claude_acp.rs @@ -8,8 +8,8 @@ use crate::acp::{ }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; -use crate::model::ModelConfig; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; +use goose_providers::model::ModelConfig; pub(crate) const CLAUDE_ACP_PROVIDER_NAME: &str = "claude-acp"; const CLAUDE_ACP_DOC_URL: &str = "https://github.com/agentclientprotocol/claude-agent-acp"; diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 0fb412220..0b85ccbb8 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -27,10 +27,10 @@ use crate::config::paths::Paths; use crate::config::search_path::SearchPaths; use crate::config::{Config, ExtensionConfig, GooseMode}; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::permission::permission_confirmation::PrincipalType; use crate::permission::{Permission, PermissionConfirmation}; use crate::subprocess::configure_subprocess; +use goose_providers::model::ModelConfig; use super::cli_common::{error_from_event, extract_usage_tokens}; diff --git a/crates/goose/src/providers/codex.rs b/crates/goose/src/providers/codex.rs index 91809d346..79352a0e4 100644 --- a/crates/goose/src/providers/codex.rs +++ b/crates/goose/src/providers/codex.rs @@ -20,9 +20,9 @@ use crate::config::paths::Paths; use crate::config::search_path::SearchPaths; use crate::config::{Config, ExtensionConfig, GooseMode}; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::subprocess::configure_subprocess; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Role; use rmcp::model::Tool; diff --git a/crates/goose/src/providers/codex_acp.rs b/crates/goose/src/providers/codex_acp.rs index e84142a84..f1631f1e7 100644 --- a/crates/goose/src/providers/codex_acp.rs +++ b/crates/goose/src/providers/codex_acp.rs @@ -8,8 +8,8 @@ use crate::acp::{ }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; -use crate::model::ModelConfig; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; +use goose_providers::model::ModelConfig; pub(crate) const CODEX_ACP_PROVIDER_NAME: &str = "codex-acp"; const CODEX_ACP_DOC_URL: &str = "https://github.com/zed-industries/codex-acp"; diff --git a/crates/goose/src/providers/copilot_acp.rs b/crates/goose/src/providers/copilot_acp.rs index 0b9336cde..2d0c03b49 100644 --- a/crates/goose/src/providers/copilot_acp.rs +++ b/crates/goose/src/providers/copilot_acp.rs @@ -8,8 +8,8 @@ use crate::acp::{ }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; -use crate::model::ModelConfig; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; +use goose_providers::model::ModelConfig; pub(crate) const COPILOT_ACP_PROVIDER_NAME: &str = "copilot-acp"; const COPILOT_ACP_DOC_URL: &str = "https://github.com/github/copilot-cli"; diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index 12477f1c7..c4e2b0461 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -14,11 +14,11 @@ use super::utils::{filter_extensions_from_system_prompt, RequestLog}; use crate::config::base::CursorAgentCommand; use crate::config::search_path::SearchPaths; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::subprocess::configure_subprocess; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; const CURSOR_AGENT_PROVIDER_NAME: &str = "cursor-agent"; diff --git a/crates/goose/src/providers/databricks.rs b/crates/goose/src/providers/databricks.rs index bd5ed4e8b..70e5fcbad 100644 --- a/crates/goose/src/providers/databricks.rs +++ b/crates/goose/src/providers/databricks.rs @@ -29,12 +29,12 @@ use super::utils::RequestLog; use crate::config::ConfigError; use crate::conversation::message::Message; use crate::instance_id::get_instance_id; -use crate::model::ModelConfig; use crate::providers::retry::{ RetryConfig, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_INITIAL_RETRY_INTERVAL_MS, DEFAULT_MAX_RETRIES, DEFAULT_MAX_RETRY_INTERVAL_MS, }; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use serde_json::json; @@ -153,8 +153,11 @@ impl DatabricksProvider { token_cache, instance_id: Self::resolve_instance_id(), }; - provider.model = - model.with_fast(DATABRICKS_DEFAULT_FAST_MODEL, DATABRICKS_PROVIDER_NAME)?; + provider.model = crate::model_config::with_configured_fast_model( + model, + DATABRICKS_PROVIDER_NAME, + DATABRICKS_DEFAULT_FAST_MODEL, + )?; Ok(provider) } diff --git a/crates/goose/src/providers/databricks_v2.rs b/crates/goose/src/providers/databricks_v2.rs index 9f0ae6b4d..69efc1f73 100644 --- a/crates/goose/src/providers/databricks_v2.rs +++ b/crates/goose/src/providers/databricks_v2.rs @@ -3,9 +3,7 @@ use async_stream::try_stream; use async_trait::async_trait; use futures::future::BoxFuture; use futures::TryStreamExt; -use goose_providers::formats::openai::{ - self, extract_reasoning_effort, is_openai_responses_model, ModelConfigParams, -}; +use goose_providers::formats::openai::{self, extract_reasoning_effort, is_openai_responses_model}; use goose_providers::images::ImageFormat; use serde::Serialize; use serde_json::Value; @@ -27,12 +25,12 @@ use super::retry::ProviderRetry; use super::utils::RequestLog; use crate::config::ConfigError; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::retry::{ RetryConfig, DEFAULT_BACKOFF_MULTIPLIER, DEFAULT_INITIAL_RETRY_INTERVAL_MS, DEFAULT_MAX_RETRIES, DEFAULT_MAX_RETRY_INTERVAL_MS, }; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; const DATABRICKS_V2_PROVIDER_NAME: &str = "databricks_v2"; @@ -260,13 +258,7 @@ impl DatabricksV2Provider { tools: &[Tool], ) -> Result { let mut payload = openai::create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index cc4f35ebd..9f7534e30 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -1,12 +1,12 @@ use crate::conversation::message::{Message, MessageContent}; use crate::mcp_utils::extract_text_from_resource; -use crate::model::ModelConfig; use crate::providers::canonical::maybe_get_canonical_model; use anyhow::{anyhow, Result}; use goose_providers::canonical::ThinkingMode; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::images::{convert_image, ImageFormat}; +use goose_providers::model::ModelConfig; use goose_providers::thinking::ThinkingEffort; use rmcp::model::{object, CallToolRequestParams, ErrorCode, ErrorData, JsonObject, Role, Tool}; use rmcp::object as json_object; @@ -52,16 +52,20 @@ pub struct AnthropicFormatOptions { impl AnthropicFormatOptions { fn for_model(self, model_config: &ModelConfig) -> Self { let preserve_thinking_context = model_config - .get_config_param::( - "preserve_thinking_context", - "ANTHROPIC_PRESERVE_THINKING_CONTEXT", - ) + .request_param::("preserve_thinking_context") + .or_else(|| { + crate::config::Config::global() + .get_param("ANTHROPIC_PRESERVE_THINKING_CONTEXT") + .ok() + }) .unwrap_or(self.preserve_thinking_context); let preserve_unsigned_thinking = model_config - .get_config_param::( - "preserve_unsigned_thinking", - "ANTHROPIC_PRESERVE_UNSIGNED_THINKING", - ) + .request_param::("preserve_unsigned_thinking") + .or_else(|| { + crate::config::Config::global() + .get_param("ANTHROPIC_PRESERVE_UNSIGNED_THINKING") + .ok() + }) .unwrap_or(self.preserve_unsigned_thinking) || preserve_thinking_context; @@ -1020,7 +1024,7 @@ where mod tests { use super::*; use crate::conversation::message::Message; - use crate::model::ModelConfig; + use goose_providers::model::ModelConfig; use rmcp::object; use serde_json::json; diff --git a/crates/goose/src/providers/formats/bedrock.rs b/crates/goose/src/providers/formats/bedrock.rs index e65f3bc5f..85f354b20 100644 --- a/crates/goose/src/providers/formats/bedrock.rs +++ b/crates/goose/src/providers/formats/bedrock.rs @@ -15,12 +15,12 @@ use rmcp::model::{ use serde_json::Value; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::providers::formats::anthropic::{ adaptive_output_effort, thinking_budget_tokens, thinking_type_for_provider, ThinkingType, ANTHROPIC_PROVIDER_NAME, }; use goose_providers::conversation::token_usage::Usage; +use goose_providers::model::ModelConfig; use once_cell::sync::Lazy; use regex::Regex; diff --git a/crates/goose/src/providers/formats/databricks.rs b/crates/goose/src/providers/formats/databricks.rs index 6d5243736..b86542c87 100644 --- a/crates/goose/src/providers/formats/databricks.rs +++ b/crates/goose/src/providers/formats/databricks.rs @@ -1,9 +1,9 @@ use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::providers::formats::anthropic::{ adaptive_output_effort, model_supports_temperature, thinking_budget_tokens, thinking_type_for_provider, ThinkingType, }; +use goose_providers::model::ModelConfig; use anyhow::{anyhow, Error}; use goose_providers::formats::openai::{ diff --git a/crates/goose/src/providers/formats/gcpvertexai.rs b/crates/goose/src/providers/formats/gcpvertexai.rs index 6e064eee0..4dbca6a03 100644 --- a/crates/goose/src/providers/formats/gcpvertexai.rs +++ b/crates/goose/src/providers/formats/gcpvertexai.rs @@ -1,8 +1,8 @@ use super::{anthropic, google}; use crate::conversation::message::Message; -use crate::model::ModelConfig; use anyhow::{Context, Result}; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use serde_json::Value; diff --git a/crates/goose/src/providers/formats/google.rs b/crates/goose/src/providers/formats/google.rs index 7d3f6b1ed..f869959d9 100644 --- a/crates/goose/src/providers/formats/google.rs +++ b/crates/goose/src/providers/formats/google.rs @@ -1,8 +1,8 @@ -use crate::model::ModelConfig; use anyhow::Result; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::{is_valid_function_name, sanitize_function_name}; +use goose_providers::model::ModelConfig; use goose_providers::thinking::ThinkingEffort; use rmcp::model::{ object, AnnotateAble, CallToolRequestParams, ErrorCode, ErrorData, RawContent, Role, Tool, @@ -560,8 +560,12 @@ fn get_thinking_config(model_config: &ModelConfig) -> Option { }) } else { let thinking_budget = match model_config - .get_config_param::("thinking_budget", "GEMINI25_THINKING_BUDGET") - { + .request_param::("thinking_budget") + .or_else(|| { + crate::config::Config::global() + .get_param("GEMINI25_THINKING_BUDGET") + .ok() + }) { Some(budget) if budget >= 0 => budget, Some(budget) => { tracing::warn!( @@ -1369,7 +1373,7 @@ data: [DONE]"#; #[test] fn test_get_thinking_config() { - use crate::model::ModelConfig; + use goose_providers::model::ModelConfig; // Test 1: Gemini 3 model with low thinking effort let mut params = std::collections::HashMap::new(); diff --git a/crates/goose/src/providers/formats/openai_responses.rs b/crates/goose/src/providers/formats/openai_responses.rs index 49a303cff..9dd46028a 100644 --- a/crates/goose/src/providers/formats/openai_responses.rs +++ b/crates/goose/src/providers/formats/openai_responses.rs @@ -1,6 +1,5 @@ use crate::conversation::message::{Message, MessageContent}; use crate::mcp_utils::extract_text_from_resource; -use crate::model::ModelConfig; use anyhow::Error; use async_stream::try_stream; use chrono; @@ -10,6 +9,7 @@ use goose_providers::errors::ProviderError; use goose_providers::formats::openai::{ extract_reasoning_effort, is_openai_responses_model, openai_reasoning_effort_for_thinking, }; +use goose_providers::model::ModelConfig; use rmcp::model::{object, CallToolRequestParams, RawContent, Role, Tool}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; @@ -945,8 +945,8 @@ where mod tests { use super::*; use crate::conversation::message::MessageContent; - use crate::model::ModelConfig; use futures::StreamExt; + use goose_providers::model::ModelConfig; use rmcp::model::CallToolRequestParams; use rmcp::object; diff --git a/crates/goose/src/providers/formats/openrouter.rs b/crates/goose/src/providers/formats/openrouter.rs index a6d68e0c4..a68465fe9 100644 --- a/crates/goose/src/providers/formats/openrouter.rs +++ b/crates/goose/src/providers/formats/openrouter.rs @@ -1,6 +1,6 @@ use crate::conversation::message::{Message, MessageContent, ProviderMetadata}; -use crate::model::ModelConfig; use goose_providers::formats::openai; +use goose_providers::model::ModelConfig; use goose_providers::thinking::ThinkingEffort; use rmcp::model::Role; use serde_json::{json, Value}; diff --git a/crates/goose/src/providers/formats/snowflake.rs b/crates/goose/src/providers/formats/snowflake.rs index 011c99b15..ba574f9ff 100644 --- a/crates/goose/src/providers/formats/snowflake.rs +++ b/crates/goose/src/providers/formats/snowflake.rs @@ -1,9 +1,9 @@ use crate::conversation::message::{Message, MessageContent}; use crate::mcp_utils::extract_text_from_resource; -use crate::model::ModelConfig; use anyhow::{anyhow, Result}; use goose_providers::conversation::token_usage::Usage; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::{object, CallToolRequestParams, Role, Tool}; use rmcp::object; use serde_json::{json, Value}; @@ -560,7 +560,7 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025 #[test] fn test_create_request_format() -> Result<()> { use crate::conversation::message::Message; - use crate::model::ModelConfig; + use goose_providers::model::ModelConfig; let model_config = ModelConfig::new_or_fail("claude-4-sonnet").with_canonical_limits("snowflake"); @@ -670,7 +670,7 @@ data: {"id":"a9537c2c-2017-4906-9817-2456168d89fa","model":"claude-sonnet-4-2025 #[test] fn test_create_request_excludes_tools_for_description() -> Result<()> { use crate::conversation::message::Message; - use crate::model::ModelConfig; + use goose_providers::model::ModelConfig; let model_config = ModelConfig::new_or_fail("claude-4-sonnet").with_canonical_limits("snowflake"); diff --git a/crates/goose/src/providers/gcpvertexai.rs b/crates/goose/src/providers/gcpvertexai.rs index 7aba01fa3..017a2dcf3 100644 --- a/crates/goose/src/providers/gcpvertexai.rs +++ b/crates/goose/src/providers/gcpvertexai.rs @@ -15,11 +15,11 @@ use tokio_util::io::StreamReader; use url::Url; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::base::{ ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, DEFAULT_PROVIDER_TIMEOUT_SECS, }; +use goose_providers::model::ModelConfig; use crate::providers::formats::gcpvertexai::{ create_request, response_to_streaming_message, GcpLocation, ModelProvider, RequestContext, diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 0d1c29aaf..0a2339b77 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -16,13 +16,13 @@ use crate::config::base::GeminiCliCommand; use crate::config::search_path::SearchPaths; use crate::config::Config; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::providers::base::ConfigKey; use crate::subprocess::configure_subprocess; use async_stream::try_stream; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Role; use rmcp::model::Tool; diff --git a/crates/goose/src/providers/gemini_oauth.rs b/crates/goose/src/providers/gemini_oauth.rs index 2c9320d27..3733856ae 100644 --- a/crates/goose/src/providers/gemini_oauth.rs +++ b/crates/goose/src/providers/gemini_oauth.rs @@ -1,6 +1,5 @@ use crate::config::paths::Paths; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::base::{ ConfigKey, MessageStream, Provider, ProviderDef, ProviderMetadata, DEFAULT_PROVIDER_TIMEOUT_SECS, @@ -8,6 +7,7 @@ use crate::providers::base::{ use crate::providers::formats::google::{create_request, response_to_streaming_message}; use crate::providers::google::GOOGLE_DOC_URL; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; const GEMINI_OAUTH_DEFAULT_MODEL: &str = "gemini-3-flash-preview"; const GEMINI_OAUTH_DEFAULT_FAST_MODEL: &str = "gemini-2.5-flash-lite"; @@ -838,7 +838,11 @@ pub struct GeminiOAuthProvider { impl GeminiOAuthProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(GEMINI_OAUTH_DEFAULT_FAST_MODEL, GEMINI_OAUTH_PROVIDER_NAME)?; + let model = crate::model_config::with_configured_fast_model( + model, + GEMINI_OAUTH_PROVIDER_NAME, + GEMINI_OAUTH_DEFAULT_FAST_MODEL, + )?; let token_provider = Arc::new(GeminiOAuthTokenProvider::new( GeminiOAuthAuthState::instance(), diff --git a/crates/goose/src/providers/githubcopilot.rs b/crates/goose/src/providers/githubcopilot.rs index 2cfbcf289..3089a2ae2 100644 --- a/crates/goose/src/providers/githubcopilot.rs +++ b/crates/goose/src/providers/githubcopilot.rs @@ -9,7 +9,7 @@ use async_trait::async_trait; use axum::http; use chrono::{DateTime, Utc}; use goose_providers::errors::ProviderError; -use goose_providers::formats::openai::{is_openai_responses_model, ModelConfigParams}; +use goose_providers::formats::openai::is_openai_responses_model; use goose_providers::images::ImageFormat; use reqwest::{Client, Response}; use serde::{Deserialize, Serialize}; @@ -36,10 +36,10 @@ use goose_providers::formats::openai::{create_request, get_usage, response_to_me use crate::config::{Config, ConfigError}; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::providers::base::{ConfigKey, MessageStream}; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; +use goose_providers::model::ModelConfig; use rmcp::model::{RawContent, Tool}; use std::ops::Deref; @@ -444,13 +444,7 @@ impl GithubCopilotProvider { if supports_streaming { let payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, @@ -486,13 +480,7 @@ impl GithubCopilotProvider { Some(session_id) }; let payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/google.rs b/crates/goose/src/providers/google.rs index 199f9c445..41ecaf763 100644 --- a/crates/goose/src/providers/google.rs +++ b/crates/goose/src/providers/google.rs @@ -6,7 +6,6 @@ use super::utils::RequestLog; use crate::conversation::message::Message; use goose_providers::errors::ProviderError; -use crate::model::ModelConfig; use crate::providers::base::{ConfigKey, Provider, ProviderDef, ProviderMetadata}; use crate::providers::formats::google::{create_request, response_to_streaming_message}; use anyhow::Result; @@ -14,6 +13,7 @@ use async_stream::try_stream; use async_trait::async_trait; use futures::future::BoxFuture; use futures::TryStreamExt; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use serde_json::Value; use std::io; @@ -67,7 +67,11 @@ pub struct GoogleProvider { impl GoogleProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(GOOGLE_DEFAULT_FAST_MODEL, GOOGLE_PROVIDER_NAME)?; + let model = crate::model_config::with_configured_fast_model( + model, + GOOGLE_PROVIDER_NAME, + GOOGLE_DEFAULT_FAST_MODEL, + )?; let config = crate::config::Config::global(); let api_key: String = config.get_secret("GOOGLE_API_KEY")?; diff --git a/crates/goose/src/providers/huggingface.rs b/crates/goose/src/providers/huggingface.rs index 166f92881..90e3974fb 100644 --- a/crates/goose/src/providers/huggingface.rs +++ b/crates/goose/src/providers/huggingface.rs @@ -8,10 +8,10 @@ use super::openai_compatible::OpenAiCompatibleProvider; use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::config::{Config, ConfigError}; use crate::conversation::message::Message; -use crate::model::ModelConfig; use anyhow::{anyhow, Result}; use futures::future::BoxFuture; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; pub const HUGGINGFACE_API_HOST: &str = "https://router.huggingface.co/v1"; @@ -109,7 +109,7 @@ impl HuggingFaceProvider { } let model = if let Some(ref fast_model_name) = config.fast_model { - model.with_fast(fast_model_name, &config.name)? + crate::model_config::with_configured_fast_model(model, &config.name, fast_model_name)? } else { model }; diff --git a/crates/goose/src/providers/init.rs b/crates/goose/src/providers/init.rs index cc329560b..90afd09fe 100644 --- a/crates/goose/src/providers/init.rs +++ b/crates/goose/src/providers/init.rs @@ -42,13 +42,13 @@ use super::{ xai_oauth::XaiOAuthProvider, }; use crate::config::ExtensionConfig; -use crate::model::ModelConfig; use crate::providers::base::ProviderType; use crate::{ config::declarative_providers::register_declarative_providers, providers::provider_registry::ProviderEntry, }; use anyhow::Result; +use goose_providers::model::ModelConfig; use tokio::sync::OnceCell; static REGISTRY: OnceCell> = OnceCell::const_new(); @@ -268,7 +268,7 @@ pub async fn create_with_named_model( model_name: &str, extensions: Vec, ) -> Result> { - let config = ModelConfig::new(model_name)?; + let config = crate::model_config::model_config_from_user_config(provider_name, model_name)?; create(provider_name, config, extensions).await } diff --git a/crates/goose/src/providers/kimicode.rs b/crates/goose/src/providers/kimicode.rs index 5f00521ce..b45ed5239 100644 --- a/crates/goose/src/providers/kimicode.rs +++ b/crates/goose/src/providers/kimicode.rs @@ -28,9 +28,9 @@ use super::openai_compatible::handle_status; use super::retry::ProviderRetry; use super::utils::RequestLog; use crate::conversation::message::Message; -use crate::model::ModelConfig; use futures::future::BoxFuture; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; const KIMI_CODE_PROVIDER_NAME: &str = "kimi_code"; @@ -163,7 +163,11 @@ impl KimiCodeProvider { } pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(KIMI_CODE_DEFAULT_FAST_MODEL, KIMI_CODE_PROVIDER_NAME)?; + let model = crate::model_config::with_configured_fast_model( + model, + KIMI_CODE_PROVIDER_NAME, + KIMI_CODE_DEFAULT_FAST_MODEL, + )?; let client = Client::builder() .timeout(StdDuration::from_secs(DEFAULT_PROVIDER_TIMEOUT_SECS)) .build()?; diff --git a/crates/goose/src/providers/litellm.rs b/crates/goose/src/providers/litellm.rs index 7a9c29317..46fb5eecd 100644 --- a/crates/goose/src/providers/litellm.rs +++ b/crates/goose/src/providers/litellm.rs @@ -17,8 +17,7 @@ use super::openai_compatible::handle_response_openai_compat; use super::retry::ProviderRetry; use super::utils::{get_model, RequestLog}; use crate::conversation::message::Message; -use crate::model::ModelConfig; -use goose_providers::formats::openai::ModelConfigParams; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; const LITELLM_PROVIDER_NAME: &str = "litellm"; @@ -229,13 +228,7 @@ impl Provider for LiteLLMProvider { Some(session_id) }; let mut payload = goose_providers::formats::openai::create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/local_inference.rs b/crates/goose/src/providers/local_inference.rs index e4911f2b3..8e5f5e444 100644 --- a/crates/goose/src/providers/local_inference.rs +++ b/crates/goose/src/providers/local_inference.rs @@ -12,7 +12,6 @@ mod tool_parsing; use crate::config::ExtensionConfig; use crate::conversation::message::{Message, MessageContent}; -use crate::model::ModelConfig; use crate::providers::base::{MessageStream, Provider, ProviderDef, ProviderMetadata}; use crate::providers::utils::RequestLog; use anyhow::Result; @@ -23,6 +22,7 @@ use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::images::ImageFormat; +use goose_providers::model::ModelConfig; use llamacpp::{LlamaCppBackend, LLAMACPP_BACKEND_ID}; use local_model_registry::ChatTemplate; use mlx::{MlxBackend, MLX_BACKEND_ID}; @@ -612,8 +612,13 @@ impl Provider for LocalInferenceProvider { // Allow request_params to override thinking let mut model_settings = model_settings; - if let Some(false) = - model_config.get_config_param::("enable_thinking", "GOOSE_LOCAL_ENABLE_THINKING") + if let Some(false) = model_config + .request_param::("enable_thinking") + .or_else(|| { + crate::config::Config::global() + .get_param("GOOSE_LOCAL_ENABLE_THINKING") + .ok() + }) { model_settings.enable_thinking = false; } diff --git a/crates/goose/src/providers/nanogpt.rs b/crates/goose/src/providers/nanogpt.rs index 7475c9df6..568122add 100644 --- a/crates/goose/src/providers/nanogpt.rs +++ b/crates/goose/src/providers/nanogpt.rs @@ -4,13 +4,13 @@ use super::openai_compatible::{handle_status, stream_openai_compat}; use super::retry::ProviderRetry; use super::utils::RequestLog; use crate::conversation::message::Message; -use crate::model::ModelConfig; use anyhow::Result; use async_trait::async_trait; use futures::future::BoxFuture; use goose_providers::errors::ProviderError; -use goose_providers::formats::openai::{create_request, ModelConfigParams}; +use goose_providers::formats::openai::create_request; use goose_providers::images::ImageFormat; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; pub const NANOGPT_PROVIDER_NAME: &str = "nano-gpt"; @@ -176,13 +176,7 @@ impl Provider for NanoGptProvider { tools: &[Tool], ) -> Result { let payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/ollama.rs b/crates/goose/src/providers/ollama.rs index 9cb8ce716..f3dbfab94 100644 --- a/crates/goose/src/providers/ollama.rs +++ b/crates/goose/src/providers/ollama.rs @@ -8,7 +8,6 @@ use super::retry::{ProviderRetry, RetryConfig}; use super::utils::RequestLog; use crate::config::declarative_providers::DeclarativeProviderConfig; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::formats::ollama::{create_request, response_to_streaming_message_ollama}; use anyhow::{Error, Result}; use async_stream::try_stream; @@ -16,8 +15,8 @@ use async_trait::async_trait; use futures::future::BoxFuture; use futures::TryStreamExt; use goose_providers::errors::ProviderError; -use goose_providers::formats::openai::ModelConfigParams; use goose_providers::images::ImageFormat; +use goose_providers::model::ModelConfig; use reqwest::Response; use rmcp::model::Tool; use serde_json::{json, Value}; @@ -220,7 +219,7 @@ impl OllamaProvider { } let model = if let Some(ref fast_model_name) = config.fast_model { - model.with_fast(fast_model_name, &config.name)? + crate::model_config::with_configured_fast_model(model, &config.name, fast_model_name)? } else { model }; @@ -300,13 +299,7 @@ impl Provider for OllamaProvider { tools: &[Tool], ) -> Result { let mut payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, @@ -550,13 +543,7 @@ mod tests { let messages = vec![crate::conversation::message::Message::user().with_text("hi")]; let payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + &model_config, "You are a helpful assistant.", &messages, &[], @@ -589,13 +576,7 @@ mod tests { let messages = vec![crate::conversation::message::Message::user().with_text("hi")]; let mut payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + &model_config, "You are a helpful assistant.", &messages, &[], @@ -639,13 +620,7 @@ mod tests { let messages = vec![crate::conversation::message::Message::user().with_text("hi")]; let mut payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + &model_config, "You are a helpful assistant.", &messages, &[], diff --git a/crates/goose/src/providers/openai.rs b/crates/goose/src/providers/openai.rs index 3c6b7ba6b..255bd15fe 100644 --- a/crates/goose/src/providers/openai.rs +++ b/crates/goose/src/providers/openai.rs @@ -17,17 +17,17 @@ use async_trait::async_trait; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::ProviderUsage; use goose_providers::errors::ProviderError; +use goose_providers::formats::openai::is_openai_responses_model; use goose_providers::formats::openai::{ create_request_with_options, get_usage, response_to_message, OpenAiFormatOptions, }; -use goose_providers::formats::openai::{is_openai_responses_model, ModelConfigParams}; use goose_providers::images::ImageFormat; use reqwest::StatusCode; use std::collections::HashMap; -use crate::model::ModelConfig; use crate::providers::base::MessageStream; use crate::providers::utils::RequestLog; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; pub(crate) const OPEN_AI_PROVIDER_NAME: &str = "openai"; @@ -243,7 +243,11 @@ impl OpenAiProvider { .map(|h| h == "api.openai.com" || h.ends_with(".api.openai.com")) .unwrap_or(false); let model = if is_openai { - model.with_fast(OPEN_AI_DEFAULT_FAST_MODEL, OPEN_AI_PROVIDER_NAME)? + crate::model_config::with_configured_fast_model( + model, + OPEN_AI_PROVIDER_NAME, + OPEN_AI_DEFAULT_FAST_MODEL, + )? } else { model }; @@ -469,7 +473,7 @@ impl OpenAiProvider { } let model = if let Some(ref fast_model_name) = config.fast_model { - model.with_fast(fast_model_name, &config.name)? + crate::model_config::with_configured_fast_model(model, &config.name, fast_model_name)? } else { model }; @@ -887,13 +891,7 @@ impl Provider for OpenAiProvider { } } else { let payload = create_request_with_options( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/openai_compatible.rs b/crates/goose/src/providers/openai_compatible.rs index b091a3979..b6bad8fb4 100644 --- a/crates/goose/src/providers/openai_compatible.rs +++ b/crates/goose/src/providers/openai_compatible.rs @@ -17,13 +17,12 @@ use super::base::{stream_from_single_message, MessageStream, Provider}; use super::retry::ProviderRetry; use super::utils::RequestLog; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::formats::openai_responses::responses_api_to_streaming_message; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::{ create_request, get_usage, response_to_message, response_to_streaming_message, - ModelConfigParams, }; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; pub struct OpenAiCompatibleProvider { @@ -66,13 +65,7 @@ impl OpenAiCompatibleProvider { for_streaming: bool, ) -> Result { create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, @@ -235,7 +228,7 @@ pub fn stream_responses_compat( #[cfg(test)] mod tests { use super::*; - use crate::model::ModelConfig; + use goose_providers::model::ModelConfig; use serde_json::json; use test_case::test_case; diff --git a/crates/goose/src/providers/openrouter.rs b/crates/goose/src/providers/openrouter.rs index 7d1ad92b0..2e42ed58b 100644 --- a/crates/goose/src/providers/openrouter.rs +++ b/crates/goose/src/providers/openrouter.rs @@ -10,10 +10,10 @@ use super::openai_compatible::{handle_status, stream_openai_compat}; use super::retry::ProviderRetry; use super::utils::RequestLog; use crate::conversation::message::Message; -use crate::model::ModelConfig; use crate::providers::formats::openrouter as openrouter_format; use goose_providers::errors::ProviderError; -use goose_providers::formats::openai::{create_request, ModelConfigParams}; +use goose_providers::formats::openai::create_request; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; pub const OPENROUTER_PROVIDER_NAME: &str = "openrouter"; @@ -48,7 +48,11 @@ pub struct OpenRouterProvider { impl OpenRouterProvider { pub async fn from_env(model: ModelConfig) -> Result { - let model = model.with_fast(OPENROUTER_DEFAULT_FAST_MODEL, OPENROUTER_PROVIDER_NAME)?; + let model = crate::model_config::with_configured_fast_model( + model, + OPENROUTER_PROVIDER_NAME, + OPENROUTER_DEFAULT_FAST_MODEL, + )?; let config = crate::config::Config::global(); let api_key: String = config.get_secret("OPENROUTER_API_KEY")?; @@ -257,13 +261,7 @@ impl Provider for OpenRouterProvider { tools: &[Tool], ) -> Result { let mut payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/pi_acp.rs b/crates/goose/src/providers/pi_acp.rs index 5bab316f7..85d1afe87 100644 --- a/crates/goose/src/providers/pi_acp.rs +++ b/crates/goose/src/providers/pi_acp.rs @@ -8,8 +8,8 @@ use crate::acp::{ }; use crate::config::search_path::SearchPaths; use crate::config::{Config, GooseMode}; -use crate::model::ModelConfig; use crate::providers::base::{current_working_dir, ProviderDef, ProviderMetadata}; +use goose_providers::model::ModelConfig; pub(crate) const PI_ACP_PROVIDER_NAME: &str = "pi-acp"; const PI_ACP_DOC_URL: &str = "https://github.com/anthropics/pi"; diff --git a/crates/goose/src/providers/provider_registry.rs b/crates/goose/src/providers/provider_registry.rs index 7900bc6f8..974a7c5fd 100644 --- a/crates/goose/src/providers/provider_registry.rs +++ b/crates/goose/src/providers/provider_registry.rs @@ -1,9 +1,9 @@ use super::base::{ConfigKey, ModelInfo, Provider, ProviderDef, ProviderMetadata, ProviderType}; use super::inventory::{InventoryIdentityInput, InventoryRegistration, InventoryResolvers}; use crate::config::{DeclarativeProviderConfig, ExtensionConfig}; -use crate::model::ModelConfig; use anyhow::Result; use futures::future::BoxFuture; +use goose_providers::model::ModelConfig; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; @@ -52,8 +52,8 @@ impl ProviderEntry { (self.inventory_configured)() } - fn normalize_model_config(&self, mut model: ModelConfig) -> ModelConfig { - model = model.with_canonical_limits(&self.metadata.name); + fn normalize_model_config(&self, mut model: ModelConfig) -> Result { + model = crate::model_config::materialize_model_config(&self.metadata.name, model)?; if model.context_limit.is_none() { if let Some(info) = self @@ -66,15 +66,18 @@ impl ProviderEntry { } } - model + Ok(model) } pub async fn create_with_default_model( &self, extensions: Vec, ) -> Result> { - let default_model = &self.metadata.default_model; - let model_config = self.normalize_model_config(ModelConfig::new(default_model.as_str())?); + let model_config = crate::model_config::model_config_from_user_config( + &self.metadata.name, + &self.metadata.default_model, + )?; + let model_config = self.normalize_model_config(model_config)?; (self.constructor)(model_config, extensions, None).await } @@ -83,7 +86,7 @@ impl ProviderEntry { model: ModelConfig, extensions: Vec, ) -> Result> { - let model = self.normalize_model_config(model); + let model = self.normalize_model_config(model)?; (self.constructor)(model, extensions, None).await } @@ -93,7 +96,7 @@ impl ProviderEntry { extensions: Vec, working_dir: PathBuf, ) -> Result> { - let model = self.normalize_model_config(model); + let model = self.normalize_model_config(model)?; (self.constructor)(model, extensions, Some(working_dir)).await } } diff --git a/crates/goose/src/providers/provider_test.rs b/crates/goose/src/providers/provider_test.rs index b8784d5ff..7bb979a76 100644 --- a/crates/goose/src/providers/provider_test.rs +++ b/crates/goose/src/providers/provider_test.rs @@ -1,4 +1,4 @@ -use crate::{conversation::message::Message, model::ModelConfig, providers::create}; +use crate::{conversation::message::Message, providers::create}; use anyhow::Result; use futures::StreamExt; use rmcp::model::ToolAnnotations; @@ -10,8 +10,7 @@ pub async fn test_provider_configuration( toolshim_enabled: bool, toolshim_model: Option, ) -> Result<()> { - let model_config = ModelConfig::new(model)? - .with_canonical_limits(provider_name) + let model_config = crate::model_config::model_config_from_user_config(provider_name, model)? .with_max_tokens(Some(50)) .with_toolshim(toolshim_enabled) .with_toolshim_model(toolshim_model); diff --git a/crates/goose/src/providers/sagemaker_tgi.rs b/crates/goose/src/providers/sagemaker_tgi.rs index bc4ba5d72..0f01bba2d 100644 --- a/crates/goose/src/providers/sagemaker_tgi.rs +++ b/crates/goose/src/providers/sagemaker_tgi.rs @@ -17,10 +17,10 @@ use crate::conversation::message::{Message, MessageContent}; use crate::session_context::SESSION_ID_HEADER; use goose_providers::errors::ProviderError; -use crate::model::ModelConfig; use chrono::Utc; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; +use goose_providers::model::ModelConfig; use rmcp::model::Role; const SAGEMAKER_TGI_PROVIDER_NAME: &str = "sagemaker_tgi"; diff --git a/crates/goose/src/providers/snowflake.rs b/crates/goose/src/providers/snowflake.rs index 7f3639a68..40df2fa65 100644 --- a/crates/goose/src/providers/snowflake.rs +++ b/crates/goose/src/providers/snowflake.rs @@ -15,8 +15,8 @@ use crate::config::ConfigError; use crate::conversation::message::Message; use goose_providers::errors::ProviderError; -use crate::model::ModelConfig; use futures::future::BoxFuture; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; const SNOWFLAKE_PROVIDER_NAME: &str = "snowflake"; diff --git a/crates/goose/src/providers/testprovider.rs b/crates/goose/src/providers/testprovider.rs index ef8cb5c52..84f71e93a 100644 --- a/crates/goose/src/providers/testprovider.rs +++ b/crates/goose/src/providers/testprovider.rs @@ -11,11 +11,11 @@ use std::sync::{Arc, Mutex}; use super::base::stream_from_single_message; use super::base::{MessageStream, Provider, ProviderDef, ProviderMetadata}; use crate::conversation::message::{Message, ToolResponse}; -use crate::model::ModelConfig; use crate::utils::bytes_to_hex; use futures::future::BoxFuture; use goose_providers::conversation::token_usage::ProviderUsage; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::{CallToolResult, Tool}; #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/goose/src/providers/tetrate.rs b/crates/goose/src/providers/tetrate.rs index 00d4e9c77..c48b1a45e 100644 --- a/crates/goose/src/providers/tetrate.rs +++ b/crates/goose/src/providers/tetrate.rs @@ -14,8 +14,8 @@ use futures::future::BoxFuture; use goose_providers::errors::ProviderError; use goose_providers::images::ImageFormat; -use crate::model::ModelConfig; -use goose_providers::formats::openai::{create_request, ModelConfigParams}; +use goose_providers::formats::openai::create_request; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use serde_json::Value; @@ -140,13 +140,7 @@ impl Provider for TetrateProvider { tools: &[Tool], ) -> Result { let payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + model_config, system, messages, tools, diff --git a/crates/goose/src/providers/toolshim.rs b/crates/goose/src/providers/toolshim.rs index 58f63297e..4e2b87570 100644 --- a/crates/goose/src/providers/toolshim.rs +++ b/crates/goose/src/providers/toolshim.rs @@ -36,13 +36,12 @@ use super::ollama::OLLAMA_DEFAULT_PORT; use super::ollama::OLLAMA_HOST; use crate::conversation::message::{Message, MessageContent}; use crate::conversation::Conversation; -use crate::model::ModelConfig; +use crate::model_config::model_config_from_user_config; use crate::providers::base::DEFAULT_PROVIDER_TIMEOUT_SECS; use anyhow::Result; use futures::StreamExt; use goose_providers::errors::ProviderError; use goose_providers::formats::openai::create_request; -use goose_providers::formats::openai::ModelConfigParams; use goose_providers::images::ImageFormat; use reqwest::Client; use rmcp::model::{object, CallToolRequestParams, RawContent, Tool}; @@ -567,9 +566,8 @@ impl LocalInterpreter { &self, format_instruction: &str, ) -> Result { - let model_config = ModelConfig::new(&self.model) + let model_config = crate::model_config::model_config_from_user_config("local", &self.model) .map_err(|e| ProviderError::RequestFailed(format!("Model config error: {e}")))? - .with_canonical_limits("local") .with_toolshim(false) .with_toolshim_model(None); @@ -693,18 +691,10 @@ impl OllamaInterpreter { let user_message = Message::user().with_text(format_instruction); messages.push(user_message); - let model_config = ModelConfig::new(model) - .map_err(|e| ProviderError::RequestFailed(format!("Model config error: {e}")))? - .with_canonical_limits("ollama"); + let model_config = model_config_from_user_config("ollama", model)?; let mut payload = create_request( - ModelConfigParams { - model_name: model_config.model_name.as_str(), - thinking_effort: model_config.thinking_effort(), - temperature: model_config.temperature, - max_tokens: model_config.max_tokens, - request_params: model_config.request_params.as_ref(), - }, + &model_config, system_prompt, &messages, &[], // No tools diff --git a/crates/goose/src/providers/xai.rs b/crates/goose/src/providers/xai.rs index 7006ad390..e6b9d529c 100644 --- a/crates/goose/src/providers/xai.rs +++ b/crates/goose/src/providers/xai.rs @@ -1,9 +1,9 @@ use super::api_client::{ApiClient, AuthMethod}; use super::base::{ConfigKey, ProviderDef, ProviderMetadata}; use super::openai_compatible::OpenAiCompatibleProvider; -use crate::model::ModelConfig; use anyhow::Result; use futures::future::BoxFuture; +use goose_providers::model::ModelConfig; const XAI_PROVIDER_NAME: &str = "xai"; pub const XAI_API_HOST: &str = "https://api.x.ai/v1"; diff --git a/crates/goose/src/providers/xai_oauth.rs b/crates/goose/src/providers/xai_oauth.rs index e0badec1a..d6f83ba15 100644 --- a/crates/goose/src/providers/xai_oauth.rs +++ b/crates/goose/src/providers/xai_oauth.rs @@ -4,7 +4,6 @@ use super::openai_compatible::OpenAiCompatibleProvider; use super::xai::{XAI_API_HOST, XAI_DEFAULT_MODEL, XAI_KNOWN_MODELS}; use crate::config::paths::Paths; use crate::conversation::message::Message; -use crate::model::ModelConfig; use anyhow::{anyhow, Result}; use async_trait::async_trait; use axum::{extract::Query, response::Html, routing::get, Router}; @@ -12,6 +11,7 @@ use base64::Engine; use chrono::{DateTime, Utc}; use futures::future::BoxFuture; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use serde::{Deserialize, Serialize}; use sha2::Digest; diff --git a/crates/goose/src/scheduler.rs b/crates/goose/src/scheduler.rs index 4cee2b1fc..4aab4dbf0 100644 --- a/crates/goose/src/scheduler.rs +++ b/crates/goose/src/scheduler.rs @@ -835,7 +835,7 @@ async fn execute_job( let provider_name = config.get_goose_provider()?; let model_name = config.get_goose_model()?; let model_config = - crate::model::ModelConfig::new(&model_name)?.with_canonical_limits(&provider_name); + crate::model_config::model_config_from_user_config(&provider_name, &model_name)?; let session = agent .config diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index f81da3c9e..e17a987e6 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -2,12 +2,12 @@ use crate::config::paths::Paths; use crate::config::GooseMode; use crate::conversation::message::Message; use crate::conversation::Conversation; -use crate::model::ModelConfig; use crate::providers::base::{Provider, MSG_COUNT_FOR_SESSION_NAME_GENERATION}; use crate::recipe::Recipe; use crate::session::extension_data::ExtensionData; use anyhow::Result; use chrono::{DateTime, Utc}; +use goose_providers::model::ModelConfig; use rmcp::model::Role; use serde::{Deserialize, Serialize}; use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; diff --git a/crates/goose/tests/acp_custom_requests_test.rs b/crates/goose/tests/acp_custom_requests_test.rs index 5b8a336b9..0453a3c16 100644 --- a/crates/goose/tests/acp_custom_requests_test.rs +++ b/crates/goose/tests/acp_custom_requests_test.rs @@ -11,9 +11,9 @@ use common_tests::fixtures::{ TestConnectionConfig, }; use goose::acp::server::AcpProviderFactory; -use goose::model::ModelConfig; use goose::providers::base::{MessageStream, Provider}; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use goose_test_support::{EnforceSessionId, IgnoreSessionId}; use serial_test::serial; use std::path::PathBuf; diff --git a/crates/goose/tests/acp_fixtures/provider.rs b/crates/goose/tests/acp_fixtures/provider.rs index 4767125b2..c11dc9247 100644 --- a/crates/goose/tests/acp_fixtures/provider.rs +++ b/crates/goose/tests/acp_fixtures/provider.rs @@ -12,10 +12,10 @@ use futures::StreamExt; use goose::acp::{AcpProvider, AcpProviderConfig}; use goose::config::{GooseMode, PermissionManager}; use goose::conversation::message::{ActionRequiredData, Message, MessageContent}; -use goose::model::ModelConfig; use goose::permission::permission_confirmation::PrincipalType; use goose::permission::{Permission, PermissionConfirmation}; use goose::providers::base::Provider; +use goose_providers::model::ModelConfig; use goose_test_support::{ExpectedSessionId, IgnoreSessionId, TEST_MODEL}; use std::collections::{HashMap, HashSet}; use std::str::FromStr; diff --git a/crates/goose/tests/acp_secret_cache_invalidation_test.rs b/crates/goose/tests/acp_secret_cache_invalidation_test.rs index 850cd06a3..bce30e4a6 100644 --- a/crates/goose/tests/acp_secret_cache_invalidation_test.rs +++ b/crates/goose/tests/acp_secret_cache_invalidation_test.rs @@ -6,11 +6,11 @@ use common_tests::fixtures::server::AcpServerConnection; use common_tests::fixtures::{run_test, send_custom, Connection, TestConnectionConfig}; use goose::config::paths::Paths; use goose::config::{Config, ConfigError}; -use goose::model::ModelConfig; use goose::providers::base::{MessageStream, Provider}; use goose::providers::inventory::ProviderInventoryService; use goose::session::session_manager::SessionStorage; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use goose_test_support::EnforceSessionId; use serial_test::serial; use std::sync::Arc; diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index f3c364b4b..f509f32ae 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -342,13 +342,13 @@ mod tests { use goose::agents::SessionConfig; use goose::config::GooseMode; use goose::conversation::message::{Message, MessageContent}; - use goose::model::ModelConfig; use goose::providers::base::{ stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata, }; use goose::session::session_manager::SessionType; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; use rmcp::model::{CallToolRequestParams, Tool}; use rmcp::object; use std::path::PathBuf; @@ -504,13 +504,13 @@ mod tests { use goose::config::base::Config; use goose::config::GooseMode; use goose::conversation::message::Message; - use goose::model::ModelConfig; use goose::providers::base::{ stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata, }; use goose::session::session_manager::SessionType; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; use rmcp::model::{AnnotateAble, CallToolRequestParams, CallToolResult, RawContent, Tool}; use std::path::PathBuf; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -857,12 +857,12 @@ mod tests { use goose::config::permission::PermissionManager; use goose::config::GooseMode; use goose::conversation::message::Message; - use goose::model::ModelConfig; use goose::providers::base::{MessageStream, Provider, ProviderDef, ProviderMetadata}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; use rmcp::model::{CallToolRequestParams, Role, Tool}; use rmcp::object; use std::path::PathBuf; @@ -1128,7 +1128,6 @@ mod tests { use goose::config::permission::PermissionManager; use goose::config::GooseMode; use goose::conversation::message::Message; - use goose::model::ModelConfig; use goose::providers::base::{ stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata, }; @@ -1136,6 +1135,7 @@ mod tests { use goose::session::SessionManager; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; use rmcp::model::Tool; use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; @@ -1495,12 +1495,12 @@ mod tests { use goose::config::permission::PermissionManager; use goose::config::GooseMode; use goose::conversation::message::Message; - use goose::model::ModelConfig; use goose::providers::base::{stream_from_single_message, MessageStream, Provider}; use goose::session::session_manager::SessionType; use goose::session::SessionManager; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; + use goose_providers::model::ModelConfig; use rmcp::model::Tool; use std::path::PathBuf; use std::sync::Arc; diff --git a/crates/goose/tests/compaction.rs b/crates/goose/tests/compaction.rs index a2d58aadd..5f9eec80e 100644 --- a/crates/goose/tests/compaction.rs +++ b/crates/goose/tests/compaction.rs @@ -5,7 +5,6 @@ use goose::agents::{Agent, AgentEvent, SessionConfig}; use goose::config::GooseMode; use goose::conversation::message::{Message, MessageContent}; use goose::conversation::Conversation; -use goose::model::ModelConfig; use goose::providers::base::{ stream_from_single_message, MessageStream, Provider, ProviderDef, ProviderMetadata, }; @@ -13,6 +12,7 @@ use goose::session::session_manager::SessionType; use goose::session::Session; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; diff --git a/crates/goose/tests/local_inference_integration.rs b/crates/goose/tests/local_inference_integration.rs index 971a76709..d52ab0c4b 100644 --- a/crates/goose/tests/local_inference_integration.rs +++ b/crates/goose/tests/local_inference_integration.rs @@ -16,8 +16,8 @@ use base64::prelude::*; use futures::StreamExt; use goose::conversation::message::Message; -use goose::model::ModelConfig; use goose::providers::create; +use goose_providers::model::ModelConfig; const DEFAULT_TEST_MODEL: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M"; @@ -28,7 +28,7 @@ fn test_model() -> String { #[tokio::test] #[ignore] async fn test_local_inference_stream_produces_output() { - let model_config = ModelConfig::new(&test_model()).expect("valid model config"); + let model_config = ModelConfig::new(test_model()).expect("valid model config"); let provider = create("local", model_config.clone(), Vec::new()) .await .expect("provider creation should succeed"); @@ -70,7 +70,7 @@ async fn test_local_inference_stream_produces_output() { #[tokio::test] #[ignore] async fn test_local_inference_large_prompt() { - let model_config = ModelConfig::new(&test_model()) + let model_config = ModelConfig::new(test_model()) .expect("valid model config") .with_max_tokens(Some(20)); let provider = create("local", model_config.clone(), Vec::new()) @@ -182,7 +182,7 @@ async fn test_local_inference_vision_produces_output() { #[tokio::test] #[ignore] async fn test_local_inference_vision_text_only_model_graceful() { - let model_config = ModelConfig::new(&test_model()).expect("valid model config"); + let model_config = ModelConfig::new(test_model()).expect("valid model config"); let provider = create("local", model_config.clone(), Vec::new()) .await .expect("provider creation should succeed"); diff --git a/crates/goose/tests/local_inference_perf.rs b/crates/goose/tests/local_inference_perf.rs index fc3502fe0..0fb4603dd 100644 --- a/crates/goose/tests/local_inference_perf.rs +++ b/crates/goose/tests/local_inference_perf.rs @@ -11,8 +11,8 @@ //! TEST_MODEL="bartowski/Qwen_Qwen3-32B-GGUF:Q4_K_M" cargo test -p goose --test local_inference_perf -- --ignored --nocapture use goose::conversation::message::Message; -use goose::model::ModelConfig; use goose::providers::create; +use goose_providers::model::ModelConfig; use std::time::Instant; const DEFAULT_TEST_MODEL: &str = "bartowski/Llama-3.2-1B-Instruct-GGUF:Q4_K_M"; @@ -24,7 +24,7 @@ fn test_model() -> String { #[tokio::test] #[ignore] async fn test_local_inference_cold_vs_warm() { - let model_config = ModelConfig::new(&test_model()) + let model_config = ModelConfig::new(test_model()) .expect("valid model config") .with_max_tokens(Some(20)); let provider = create("local", model_config.clone(), Vec::new()) diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index 0426592c1..859e11362 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -13,7 +13,7 @@ use tokio_util::sync::CancellationToken; use goose::agents::extension::{Envs, ExtensionConfig}; use goose::agents::extension_manager::{ExtensionManager, ExtensionManagerCapabilities}; use goose::agents::GoosePlatform; -use goose::model::ModelConfig; +use goose_providers::model::ModelConfig; use test_case::test_case; diff --git a/crates/goose/tests/providers.rs b/crates/goose/tests/providers.rs index 24f49d8aa..b215c7d3c 100644 --- a/crates/goose/tests/providers.rs +++ b/crates/goose/tests/providers.rs @@ -289,7 +289,7 @@ impl ProviderFixture { async fn tool_roundtrip( &self, prompt: &str, - model_config: Option, + model_config: Option, ) -> Result { let tools = self .agent @@ -444,9 +444,9 @@ impl ProviderFixture { async fn test_image_content_support(&self) -> Result<()> { let image_config = match &self.image_model { - Some(model) => { - Some(goose::model::ModelConfig::new(model)?.with_canonical_limits(&self.name)) - } + Some(model) => Some( + goose_providers::model::ModelConfig::new(model)?.with_canonical_limits(&self.name), + ), None => None, }; let response = self @@ -467,7 +467,8 @@ impl ProviderFixture { async fn test_model_switch(&self) -> Result<()> { let default = &self.provider.get_model_config().model_name; let alt = self.model_switch_name.as_deref().unwrap(); - let alt_config = goose::model::ModelConfig::new(alt)?.with_canonical_limits(&self.name); + let alt_config = + goose_providers::model::ModelConfig::new(alt)?.with_canonical_limits(&self.name); let message = Message::user().with_text("Just say hello!"); let (response, _) = self diff --git a/crates/goose/tests/session_id_propagation_test.rs b/crates/goose/tests/session_id_propagation_test.rs index c8c069f15..74bff73e9 100644 --- a/crates/goose/tests/session_id_propagation_test.rs +++ b/crates/goose/tests/session_id_propagation_test.rs @@ -1,9 +1,9 @@ use goose::conversation::message::Message; -use goose::model::ModelConfig; use goose::providers::api_client::{ApiClient, AuthMethod}; use goose::providers::base::Provider; use goose::providers::openai::OpenAiProvider; use goose::session_context::SESSION_ID_HEADER; +use goose_providers::model::ModelConfig; use serde_json::json; use std::sync::Arc; use std::sync::Mutex; diff --git a/crates/goose/tests/tetrate_streaming.rs b/crates/goose/tests/tetrate_streaming.rs index 04e7370c7..dae08d988 100644 --- a/crates/goose/tests/tetrate_streaming.rs +++ b/crates/goose/tests/tetrate_streaming.rs @@ -1,9 +1,9 @@ use anyhow::Result; use futures::StreamExt; use goose::conversation::message::{Message, MessageContent}; -use goose::model::ModelConfig; use goose::providers::base::Provider; use goose::providers::tetrate::TetrateProvider; +use goose_providers::model::ModelConfig; use rmcp::model::Tool; use rmcp::object; use serial_test::serial; diff --git a/documentation/docs/getting-started/providers.md b/documentation/docs/getting-started/providers.md index 4cd3a39d3..494f9d25e 100644 --- a/documentation/docs/getting-started/providers.md +++ b/documentation/docs/getting-started/providers.md @@ -1459,7 +1459,7 @@ When thinking is enabled, you can view the model's reasoning process. See [Viewi :::info Priority Order The thinking level is determined in this order (highest to lowest priority): -1. `request_params.thinking_level` in model configuration (via `GOOSE_PREDEFINED_MODELS`) +1. `request_params.thinking_level` in model configuration 2. `GEMINI3_THINKING_LEVEL` environment variable 3. Default value: `low` ::: diff --git a/documentation/docs/guides/environment-variables.md b/documentation/docs/guides/environment-variables.md index 2583445d3..9e7a06ca1 100644 --- a/documentation/docs/guides/environment-variables.md +++ b/documentation/docs/guides/environment-variables.md @@ -60,80 +60,6 @@ export GOOSE_PROVIDER__HOST="https://api.anthropic.com" export GOOSE_PROVIDER__API_KEY="your-api-key-here" ``` -### Custom Model Definitions - -Define custom model configurations with provider-specific parameters and context limits. This is useful for enabling provider beta features (like extended context windows) or configuring models with specific settings. - -| Variable | Purpose | Values | Default | -|----------|---------|---------|---------| -| `GOOSE_PREDEFINED_MODELS` | Define custom model configurations | JSON array of model objects | None | - -**Model Configuration Fields:** - -| Field | Required | Type | Description | -|-------|----------|------|-------------| -| `id` | No | number | Optional numeric identifier | -| `name` | Yes | string | Model name used to reference this configuration | -| `provider` | Yes | string | Provider name (e.g., "databricks", "openai", "anthropic") | -| `alias` | No | string | Display name for the model | -| `subtext` | No | string | Additional descriptive text | -| `context_limit` | No | number | Override the default context window size in tokens | -| `request_params` | No | object | Provider-specific parameters included in API requests | - -:::info -The `id`, `alias`, and `subtext` fields are currently not used. -::: - -When a custom model's `context_limit` is specified, it takes precedence over pattern-matching but can still be overridden by explicit environment variables like [`GOOSE_CONTEXT_LIMIT`](#model-context-limit-overrides). - -**Examples** - -```bash -# Enable Anthropic's 1M context window with beta header -export GOOSE_PREDEFINED_MODELS='[ - { - "id": 1, - "name": "claude-sonnet-4-1m", - "provider": "anthropic", - "alias": "Claude Sonnet 4 (1M context)", - "subtext": "Anthropic", - "context_limit": 1000000, - "request_params": { - "anthropic_beta": ["context-1m-2025-08-07"] - } - } -]' - -# Define multiple custom models -export GOOSE_PREDEFINED_MODELS='[ - { - "id": 1, - "name": "gpt-4-custom", - "provider": "openai", - "alias": "GPT-4 (200k)", - "context_limit": 200000 - }, - { - "id": 2, - "name": "internal-model", - "provider": "databricks", - "alias": "Internal Model (500k)", - "context_limit": 500000 - } -]' - -# Gemini 3 with high thinking level -export GOOSE_PREDEFINED_MODELS='[ - { - "name": "gemini-3-pro", - "provider": "google", - "request_params": {"thinking_level": "high"} - } -]' -``` - -Custom context limits and request parameters are applied when the model is used. Custom context limits are displayed in goose CLI's [token usage indicator](/docs/guides/sessions/smart-context-management#token-usage). - ### Claude Thinking Configuration These variables control Claude's reasoning behavior. Supported on Anthropic and Databricks providers.